Compare commits

..

11 Commits

Author SHA1 Message Date
d2e5e8f69d Add dynamic page title 2026-02-17 18:25:11 +13:00
817c857fde remove alert from delete function 2026-02-17 16:33:37 +13:00
f30262be3b order entries list by date 2026-02-17 16:31:51 +13:00
f4d503c335 increase size of image 2026-02-17 16:13:54 +13:00
70bd76f49e always show editor 2026-02-17 12:37:17 +13:00
c428aae077 fix create new entry endpoint 2026-02-17 12:36:02 +13:00
89d2aaca75 EDITOR WORK COMPLETE 2026-02-17 12:35:46 +13:00
4413374429 default editor to today 2026-02-17 11:58:22 +13:00
2a953ef5ce Add param matcher to ensure random paths dont crash the app 2026-02-17 11:34:23 +13:00
0b038f4dea okay working now, datevalue respects current page 2026-02-17 11:22:38 +13:00
1d6e5b5d4c Just need a checkpoint cos nothings working atm 2026-02-17 11:13:03 +13:00
16 changed files with 423 additions and 88 deletions

View File

@@ -1,52 +1,319 @@
<script lang="ts">
import { CalendarDate } from "@internationalized/date";
import {
CalendarDate,
today,
getLocalTimeZone,
} from "@internationalized/date";
import { onMount } from "svelte";
import { formatDate } from "$lib/date";
import { createEntry, updateEntry, deleteEntry } from "$lib/upload.ts";
import { createEntry, updateEntry, deleteEntry } from "$lib/upload";
import Button from "$lib/components/ui/button/button.svelte";
import { Textarea } from "$lib/components/ui/textarea";
import { Upload, X } from "@lucide/svelte";
import { marked } from "marked";
let {
dateValue = $bindable<CalendarDate | undefined>(
new CalendarDate(2026, 2, 1),
),
} = $props();
let { dateValue = $bindable<CalendarDate>(today(getLocalTimeZone())) } =
$props();
let edit = $state<boolean>(false);
let isLoading = $state<boolean>(false);
let error = $state<string>("");
let previewImage = $state<string | null>(null);
let fileInput = $state<HTMLInputElement | undefined>(undefined);
let entry = $state({
id: "",
date: dateValue.toString(),
image: "",
content: "",
});
let editingEntry = $state({
id: "",
date: dateValue.toString(),
image: "",
content: "",
});
onMount(async () => {
const res = await fetch(`/api/entry?date=${dateValue.toString()}`);
const data = await res.json();
if (data) {
entry = { date: dateValue.toString(), ...data[0] };
}
await loadEntry();
});
async function loadEntry() {
try {
error = "";
const res = await fetch(`/api/entry?date=${dateValue.toString()}`);
const data = await res.json();
if (data && data.length > 0) {
entry = { ...data[0], date: dateValue.toString() };
previewImage = entry.image || null;
} else {
entry = {
id: "",
date: dateValue.toString(),
image: "",
content: "",
};
previewImage = null;
}
} catch (err) {
error = "Failed to load entry";
console.error(err);
}
}
function startEdit() {
editingEntry = { ...entry };
previewImage = entry.image || null;
edit = true;
}
function cancelEdit() {
edit = false;
error = "";
previewImage = entry.image || null;
}
function handleImageSelect(e: Event) {
const target = e.target as HTMLInputElement;
const files = target.files;
if (files && files.length > 0) {
const file = files[0];
// Validate file type
if (!file.type.startsWith("image/")) {
error = "Please select a valid image file";
return;
}
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
error = "Image must be smaller than 5MB";
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result as string;
editingEntry.image = result;
previewImage = result;
error = "";
};
reader.onerror = () => {
error = "Failed to read image file";
};
reader.readAsDataURL(file);
}
}
function removeImage() {
editingEntry.image = "";
previewImage = null;
if (fileInput) {
fileInput.value = "";
}
}
async function handleSave() {
try {
isLoading = true;
error = "";
editingEntry.date = dateValue.toString();
if (entry.id) {
editingEntry.id = entry.id;
await updateEntry(editingEntry);
} else {
delete editingEntry.id;
await createEntry(editingEntry);
}
// Reload the entry to get updated data
await loadEntry();
edit = false;
} catch (err) {
error = "Failed to save entry. Please try again.";
console.error(err);
} finally {
isLoading = false;
}
}
async function handleDelete() {
if (!confirm("Are you sure you want to delete this entry?")) {
return;
}
try {
isLoading = true;
error = "";
await deleteEntry(entry);
await loadEntry();
edit = false;
} catch (err) {
error = "Failed to delete entry. Please try again.";
console.error(err);
} finally {
isLoading = false;
}
}
</script>
<div class="flex flex-row items-center justify-between">
<h1 class="text-3xl">{formatDate(entry.date)}</h1>
<div class="w-full max-w-2xl">
<!-- Header -->
<div class="flex flex-row items-center justify-between mb-6">
<h1 class="text-3xl font-bold">{formatDate(entry.date)}</h1>
<div class="flex gap-2">
{#if !edit}
<Button variant="secondary" onclick={startEdit}>Edit</Button>
{:else}
<Button
variant="destructive"
onclick={handleDelete}
disabled={isLoading}
>
Delete
</Button>
<Button
variant="default"
onclick={handleSave}
disabled={isLoading}
>
{isLoading ? "Saving..." : "Save"}
</Button>
<Button
variant="outline"
onclick={cancelEdit}
disabled={isLoading}
>
Cancel
</Button>
{/if}
</div>
</div>
<!-- Error Message -->
{#if error}
<div
class="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-destructive text-sm"
>
{error}
</div>
{/if}
<!-- View Mode -->
{#if !edit}
<button
class="btn bg-secondary py-2 px-3 rounded-lg hover:opacity-60 transition-brightness"
onclick={() => (edit = !edit)}>Edit</button
>
<div class="space-y-4">
<!-- Image Display -->
{#if entry.image}
<div class="rounded-lg overflow-hidden border border-input">
<img
src={entry.image}
alt="Entry"
class="w-full h-auto max-h-128 object-cover"
/>
</div>
{:else}
<div
class="rounded-lg border-2 border-dashed border-muted-foreground/25 bg-muted/50 h-48 flex items-center justify-center text-muted-foreground text-sm"
>
No image
</div>
{/if}
<!-- Content Display -->
{#if entry.content}
<div class="rounded-lg border border-input bg-muted/30 p-4">
<div class="text-base whitespace-pre-wrap text-foreground">
{@html marked(entry.content)}
</div>
</div>
{:else}
<div
class="rounded-lg border border-dashed border-muted-foreground/25 bg-muted/50 p-4 text-muted-foreground text-sm text-center"
>
No content yet
</div>
{/if}
</div>
<!-- Edit Mode -->
{:else}
<button class="bg-destructive py-2 px-3 rounded-lg" onclick={deleteEntry}>
delete
</button>
<button
class="bg-secondary py-2 px-3 rounded-lg"
onclick={() => (edit = false)}
>
cancel
</button>
<button class="bg-primary text-accent py-2 px-3 rounded-lg">
save
</button>
<div class="space-y-6">
<!-- Image Picker -->
<div class="space-y-3">
<!-- <label for="image-input" class="text-sm font-medium">
Image
</label> -->
{#if previewImage}
<div
class="relative rounded-lg overflow-hidden border border-input"
>
<img
src={previewImage}
alt="Preview"
class="w-full h-auto max-h-128 object-cover"
/>
<button
type="button"
onclick={removeImage}
class="absolute top-2 right-2 p-2 bg-destructive text-white rounded-md hover:bg-destructive/90 transition-colors"
title="Remove image"
>
<X size={20} />
</button>
</div>
{:else}
<div
class="rounded-lg border-2 border-dashed border-muted-foreground/25 bg-muted/50 p-8 flex flex-col items-center justify-center gap-3 cursor-pointer hover:border-muted-foreground/50 transition-colors"
role="button"
tabindex={0}
onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
fileInput?.click();
}
}}
onclick={() => fileInput?.click()}
>
<Upload size={32} class="text-muted-foreground" />
<p class="text-sm font-medium text-muted-foreground">
Click to upload or drag and drop
</p>
<p class="text-xs text-muted-foreground">
PNG, JPG, GIF up to 5MB
</p>
</div>
{/if}
<input
bind:this={fileInput}
id="image-input"
type="file"
accept="image/*"
onchange={handleImageSelect}
class="hidden"
/>
</div>
<!-- Content Editor -->
<div class="space-y-3">
<!-- <label for="content-input" class="text-sm font-medium">
Content
</label> -->
<Textarea
id="content-input"
bind:value={editingEntry.content}
placeholder="How was your day?"
class="min-h-64"
/>
<p class="text-xs text-muted-foreground">
{editingEntry.content.length} characters
</p>
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,2 @@
export { default as Input } from "./input.svelte";
export type { InputProps } from "./input.svelte";

View File

@@ -0,0 +1,25 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLInputAttributes } from "svelte/elements";
export type InputProps = WithElementRef<HTMLInputAttributes>;
</script>
<script lang="ts">
let {
class: className,
type = "text",
ref = $bindable(null),
...restProps
}: InputProps = $props();
</script>
<input
bind:this={ref}
{type}
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,2 @@
export { default as Textarea } from "./textarea.svelte";
export type { TextareaProps } from "./textarea.svelte";

View File

@@ -0,0 +1,28 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLTextareaAttributes } from "svelte/elements";
export type TextareaProps = WithElementRef<HTMLTextareaAttributes> & {
class?: string;
value?: string;
};
</script>
<script lang="ts">
let {
class: className,
value = $bindable(""),
ref = $bindable(null),
...restProps
}: TextareaProps = $props();
</script>
<textarea
bind:this={ref}
bind:value
class={cn(
"flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...restProps}
></textarea>

View File

@@ -57,17 +57,11 @@ export async function updateEntry(entry) {
}
export async function deleteEntry(entry) {
const res = await fetch(`/api/entry/delete`, {
await fetch(`/api/entry/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: entry.id })
});
if (res.ok) {
entry = null;
} else {
alert('Failed to delete entry');
}
}

4
src/params/date.ts Normal file
View File

@@ -0,0 +1,4 @@
export function match(value) {
return /^\d{4}-\d{2}-\d{2}$/.test(value)
}

View File

@@ -2,32 +2,39 @@
import "./layout.css";
import favicon from "$lib/assets/favicon.svg";
import { ModeWatcher } from "mode-watcher";
import { today, getLocalTimeZone } from "@internationalized/date";
import {
CalendarDate,
today,
getLocalTimeZone,
} from "@internationalized/date";
import Header from "$lib/components/header.svelte";
import Calendar from "$lib/components/calendar.svelte";
import EntrySummaryView from "$lib/components/entrySummaryView.svelte";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
let { children } = $props();
let dateValue = $state(today(getLocalTimeZone()));
let entries = $state([])
let { children, data } = $props();
let dateValue = $derived(data.date);
let entries = $derived(data.entries);
let title = $state("Memento")
onMount(async () => {
const res = await fetch(
`/api/entry?month=${dateValue.year}-${dateValue.month}`,
);
entries = await res.json()
});
$effect(() => {
goto(`/${dateValue}`)
})
// Navigate when dateValue changes
goto(`/${dateValue}`);
title = `Memento: ${dateValue}`
});
$effect(() => {
// Sync dateValue with data.date when it changes
dateValue = data.date;
});
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
<svelte:head>
<title>{title}</title>
<link rel="icon" href={favicon} />
</svelte:head>
<ModeWatcher />
<div
@@ -37,7 +44,6 @@
<Header />
{#key dateValue}
<!-- <Editor {dateValue} /> -->
{@render children()}
{/key}
</div>
@@ -46,11 +52,8 @@
<Calendar bind:value={dateValue} />
<ul class="space-y-4">
{#each entries as entry}
<EntrySummaryView
{entry}
bind:value={dateValue}
/>
<EntrySummaryView {entry} bind:value={dateValue} />
{/each}
</ul>
</div>
</div>
</div>

22
src/routes/+layout.ts Normal file
View File

@@ -0,0 +1,22 @@
import { today, getLocalTimeZone, CalendarDate } from "@internationalized/date";
export async function load({ params, fetch }) {
let dateValue;
if (params.date) {
const [year, month, day] = params.date.split("-").map(Number);
dateValue = new CalendarDate(year, month, day);
} else {
dateValue = today(getLocalTimeZone());
}
const res = await fetch(
`/api/entry?month=${dateValue.year}-${dateValue.month}`,
);
const entries = await res.json();
return {
entries: entries,
date: dateValue,
};
}

View File

@@ -1,13 +0,0 @@
export const load = async ({ fetch, params }) => {
const res = await fetch("/api/entry/all");
const allEntries = await res.json();
const today = new Date().toISOString().split('T')[0];
const todayEntry = await fetch(`/api/entry?date=${today}`);
const todayEntryData = await todayEntry.json();
return {
all: allEntries,
today: todayEntryData,
};
};

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import Editor from "$lib/components/editor.svelte";
let {
data
} = $props()
</script>
<Editor dateValue={data.date} />

View File

@@ -0,0 +1,6 @@
export async function load({ params, fetch }) {
const res = await fetch(`/api/entry?date=${params.date}`);
const entries = await res.json();
return { entry: entries[0] };
}

View File

@@ -1,8 +0,0 @@
<script lang="ts">
let {
data
} = $props()
</script>
{JSON.stringify(data)}

View File

@@ -1,6 +0,0 @@
export async function load({ params, fetch }) {
const res = await fetch(`/api/entry?date=${params.date}`);
const entries = await res.json();
return { entries };
}

View File

@@ -61,7 +61,7 @@ async function getEntryByMonth(monthString: string) {
const endDate = new Date(year, month, 1, 0, 0, 0, 0)
const entries = await db.select().from(entryTable).where(
sql`${entryTable.date} >= ${startDate.toISOString()}::timestamp AND ${entryTable.date} < ${endDate.toISOString()}::timestamp`
sql`${entryTable.date} >= ${startDate.toISOString()}::timestamp AND ${entryTable.date} < ${endDate.toISOString()}::timestamp ORDER BY ${entryTable.date} DESC`
)
return httpResponse(entries, 200)

View File

@@ -9,7 +9,6 @@ export async function POST({ request }) {
try {
const body = await request.json();
const entry: typeof entryTable.$inferInsert = body
entry.date = new Date(body.date)
await db.insert(entryTable).values(entry)