feat: ui integration for note expiration dates (closes #5)
This commit is contained in:
parent
8535d011d0
commit
dd2d8f3c34
@ -15,6 +15,7 @@
|
||||
--light-foreground: #e0e0e0;
|
||||
--light-accent: #303052;
|
||||
--light-text: rgb(34, 40, 49);
|
||||
--light-highlight-text: #c28e4a;
|
||||
--light-error-text: #4b0000;
|
||||
--light-error-background: #fcaaaae1;
|
||||
--light-success-text: #004b0a;
|
||||
@ -24,6 +25,7 @@
|
||||
--dark-foreground: #222222;
|
||||
--dark-accent: #bebff7;
|
||||
--dark-text: rgb(238, 238, 238);
|
||||
--dark-highlight-text: #f7debe;
|
||||
--dark-error-text: #fcadaa;
|
||||
--dark-error-background: #4b0000e0;
|
||||
--dark-success-text: #adfcaa;
|
||||
@ -499,6 +501,14 @@
|
||||
@apply font-copernicus max-w-full pb-1 text-2xl wrap-break-word;
|
||||
}
|
||||
|
||||
.note-title-exp-highlight {
|
||||
@apply text-[var(--light-highlight-text)];
|
||||
}
|
||||
|
||||
.dark .note-title-exp-highlight {
|
||||
@apply text-[var(--dark-highlight-text)];
|
||||
}
|
||||
|
||||
/* Limit title (display as input is always only one row tall) to max. 4 rows tall on short screens */
|
||||
@media (max-width: 768px) {
|
||||
.note-title-display {
|
||||
|
@ -10,6 +10,7 @@
|
||||
import ViewEye from "$lib/icons/editor/ViewEye.svelte"
|
||||
import Save from "$lib/icons/editor/Save.svelte"
|
||||
import type { FullNote } from "$lib/logic/model"
|
||||
import { formatTitleWithHighlight } from "$lib/util/contentVisual"
|
||||
|
||||
// props
|
||||
export let note: FullNote
|
||||
@ -20,10 +21,12 @@
|
||||
|
||||
// constants
|
||||
const TITLE_MAX_LENGTH = 150
|
||||
const ZEROWIDTH_CHARACTERS = /^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/
|
||||
|
||||
// local state
|
||||
let editableTitle = note.title
|
||||
let editableContent = note.content
|
||||
let displayTitle = formatTitleWithHighlight(note.title)
|
||||
|
||||
// markdown parser with syntax highlight support
|
||||
const marked = new Marked(
|
||||
@ -42,6 +45,7 @@
|
||||
$: if (note && note.id) {
|
||||
editableTitle = note.title
|
||||
editableContent = note.content
|
||||
displayTitle = formatTitleWithHighlight(note.title)
|
||||
}
|
||||
|
||||
const handleContentChange = (
|
||||
@ -110,6 +114,8 @@
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
// only update the display title once the user saves the edits
|
||||
displayTitle = formatTitleWithHighlight(editableTitle)
|
||||
saveNote(editableTitle, editableContent)
|
||||
}
|
||||
|
||||
@ -135,7 +141,7 @@
|
||||
})
|
||||
|
||||
// remove the most common zerowidth characters from the start of the file
|
||||
marked.parse(markdown.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, ""))
|
||||
marked.parse(markdown.replace(ZEROWIDTH_CHARACTERS, ""))
|
||||
|
||||
return await marked.parse(markdown)
|
||||
}
|
||||
@ -181,7 +187,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<h1 class="note-title-display">
|
||||
{note.title || "Untitled Note"}
|
||||
{@html displayTitle}
|
||||
</h1>
|
||||
{/if}
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import Close from "$lib/icons/Close.svelte"
|
||||
import { isPasswordValid } from "$lib/util/authValidation"
|
||||
import { onMount } from "svelte"
|
||||
import { formatDate } from "$lib/util/contentVisual"
|
||||
import { formatDateLong } from "$lib/util/contentVisual"
|
||||
|
||||
// props
|
||||
export let onClose: () => void
|
||||
@ -163,13 +163,13 @@
|
||||
<tr class="modal-table-row">
|
||||
<th class="modal-table-row-item modal-table-head">Account created</th>
|
||||
<td class="modal-table-row-item"
|
||||
>{userData?.createdAt !== undefined ? formatDate(userData?.createdAt) : ""}</td
|
||||
>{userData?.createdAt !== undefined ? formatDateLong(userData?.createdAt) : ""}</td
|
||||
>
|
||||
</tr>
|
||||
<tr class="modal-table-row">
|
||||
<th class="modal-table-row-item modal-table-head">Password updated</th>
|
||||
<td class="modal-table-row-item"
|
||||
>{userData?.createdAt !== undefined ? formatDate(userData?.updatedAt) : ""}</td
|
||||
>{userData?.createdAt !== undefined ? formatDateLong(userData?.updatedAt) : ""}</td
|
||||
>
|
||||
</tr>
|
||||
{#if userData?.isAdmin}
|
||||
|
@ -13,7 +13,7 @@
|
||||
import AdminWrench from "$lib/icons/sidebar/AdminShield.svelte"
|
||||
import Exit from "$lib/icons/sidebar/Exit.svelte"
|
||||
import ThemeToggle from "./ThemeToggle.svelte"
|
||||
import { formatDate } from "$lib/util/contentVisual"
|
||||
import { formatDateLong, formatDateShort, parseExpirationPrefix } from "$lib/util/contentVisual"
|
||||
import type { FullNote, NoteMetadata } from "$lib/logic/model"
|
||||
|
||||
// props
|
||||
@ -172,7 +172,7 @@
|
||||
>
|
||||
<div class="flex w-full items-start justify-between">
|
||||
<h3 class="sidebar-list-item-title">
|
||||
{note.title || "Untitled Note"}
|
||||
{parseExpirationPrefix(note.title)[0] || "Untitled Note"}
|
||||
</h3>
|
||||
|
||||
<!-- Delete button -->
|
||||
@ -186,7 +186,11 @@
|
||||
</div>
|
||||
|
||||
<p class="sidebar-list-item-metadata">
|
||||
{formatDate(note.updatedAt)}
|
||||
{formatDateLong(note.updatedAt)}
|
||||
</p>
|
||||
|
||||
<p class="sidebar-list-item-metadata">
|
||||
Expires {note.expiresAt !== null ? formatDateShort(note.expiresAt) : "never"}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
|
@ -15,10 +15,7 @@ import {
|
||||
} from "./model"
|
||||
|
||||
const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||
const UUID_REGEX = new RegExp(
|
||||
"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
|
||||
"i"
|
||||
)
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
|
||||
// lifetimes of *in-memory* authentication tokens in milliseconds
|
||||
const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
||||
|
@ -29,6 +29,7 @@ export interface ApiFullNoteResponse {
|
||||
content: string
|
||||
version_number: number
|
||||
version_created_at: string
|
||||
note_expires_at: string
|
||||
note_created_at: string
|
||||
note_updated_at: string
|
||||
}
|
||||
@ -41,6 +42,7 @@ export class FullNote {
|
||||
versionNumber: number
|
||||
versionCreatedAt: Date
|
||||
isActiveVersion: boolean
|
||||
noteExpiresAt: Date | null
|
||||
noteCreatedAt: Date
|
||||
noteUpdatedAt: Date
|
||||
|
||||
@ -52,6 +54,8 @@ export class FullNote {
|
||||
this.versionNumber = apiResponse.version_number
|
||||
this.versionCreatedAt = new Date(apiResponse.version_created_at)
|
||||
this.isActiveVersion = true // this endpoint serves only the latest version
|
||||
this.noteExpiresAt =
|
||||
apiResponse.note_expires_at !== null ? new Date(apiResponse.note_expires_at) : null
|
||||
this.noteCreatedAt = new Date(apiResponse.note_created_at)
|
||||
this.noteUpdatedAt = new Date(apiResponse.note_updated_at)
|
||||
}
|
||||
@ -64,6 +68,7 @@ export class FullNote {
|
||||
content: apiResponse.content,
|
||||
version_number: apiResponse.version_number,
|
||||
version_created_at: apiResponse.created_at,
|
||||
note_expires_at: this.noteExpiresAt ? this.noteExpiresAt.toISOString() : "",
|
||||
note_created_at: this.noteCreatedAt.toISOString(),
|
||||
note_updated_at: this.noteUpdatedAt.toISOString()
|
||||
})
|
||||
@ -79,6 +84,7 @@ export interface ApiNoteMetadataResponse {
|
||||
note_id: string
|
||||
owner_id: string
|
||||
title: string
|
||||
expires_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
@ -86,12 +92,14 @@ export class NoteMetadata {
|
||||
id: string
|
||||
owner: string
|
||||
title: string
|
||||
expiresAt: Date | null
|
||||
updatedAt: Date
|
||||
|
||||
constructor(apiResponse: ApiNoteMetadataResponse) {
|
||||
this.id = apiResponse.note_id
|
||||
this.owner = apiResponse.owner_id
|
||||
this.title = apiResponse.title
|
||||
this.expiresAt = apiResponse.expires_at !== null ? new Date(apiResponse.expires_at) : null
|
||||
this.updatedAt = new Date(apiResponse.updated_at)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
const MIN_USERNAME_LENGTH = 3
|
||||
const MAX_USERNAME_LENGTH = 20
|
||||
const USERNAME_REGEX = RegExp("^[a-z0-9_]+$")
|
||||
const USERNAME_REGEX = /^[a-z0-9_]+$/
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 12
|
||||
const MAX_PASSWORD_LENGTH = 72
|
||||
|
@ -1,4 +1,7 @@
|
||||
export const formatDate = (dateString: string | Date): string => {
|
||||
// Format: '@exp:2025-06-15' or '@exp:+7d' (or '@expires')
|
||||
const EXPIRATION_DATE_REGEX = /^(@(?:exp|expires):(?:(?:\d{4}-\d{2}-\d{2})|(?:\+\d+[dwmy])))\s*(.*)/
|
||||
|
||||
export const formatDateLong = (dateString: string | Date): string => {
|
||||
if (!dateString) {
|
||||
return ""
|
||||
}
|
||||
@ -13,3 +16,37 @@ export const formatDate = (dateString: string | Date): string => {
|
||||
minute: "numeric"
|
||||
})
|
||||
}
|
||||
|
||||
export const formatDateShort = (dateString: string | Date): string => {
|
||||
if (!dateString) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const d = new Date(dateString)
|
||||
return d.toLocaleDateString(undefined, {
|
||||
weekday: "short",
|
||||
year: "2-digit",
|
||||
month: "short"
|
||||
})
|
||||
}
|
||||
|
||||
export const parseExpirationPrefix = (title: string): [string, string] => {
|
||||
const match = title.match(EXPIRATION_DATE_REGEX)
|
||||
|
||||
if (match && match[1]) {
|
||||
console.log(`[UTIL] Extracted expiration prefix: '${match[1]}'`)
|
||||
// return [clean title, expiration prefix]
|
||||
return [match[2], match[1]]
|
||||
}
|
||||
|
||||
return [title, ""]
|
||||
}
|
||||
|
||||
export const formatTitleWithHighlight = (title: string): string => {
|
||||
const [cleanTitle, expirationPrefix] = parseExpirationPrefix(title)
|
||||
if (expirationPrefix === "") {
|
||||
return title
|
||||
}
|
||||
|
||||
return `<span class="note-title-exp-highlight">${expirationPrefix}</span> ${cleanTitle}`
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user