Compare commits

...

3 Commits

Author SHA1 Message Date
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
7 changed files with 355 additions and 42 deletions

View File

@@ -1,56 +1,319 @@
<script lang="ts"> <script lang="ts">
import { CalendarDate, today, getLocalTimeZone } from "@internationalized/date"; import {
CalendarDate,
today,
getLocalTimeZone,
} from "@internationalized/date";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "$lib/date"; 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 { let { dateValue = $bindable<CalendarDate>(today(getLocalTimeZone())) } =
dateValue = $bindable<CalendarDate>( today(getLocalTimeZone()) ), $props();
} = $props();
let edit = $state<boolean>(false); 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({ let entry = $state({
id: "",
date: dateValue.toString(),
image: "",
content: "",
});
let editingEntry = $state({
id: "",
date: dateValue.toString(), date: dateValue.toString(),
image: "", image: "",
content: "", content: "",
}); });
onMount(async () => { onMount(async () => {
await loadEntry();
});
async function loadEntry() {
try {
error = "";
const res = await fetch(`/api/entry?date=${dateValue.toString()}`); const res = await fetch(`/api/entry?date=${dateValue.toString()}`);
const data = await res.json(); const data = await res.json();
if (data) { if (data && data.length > 0) {
entry = { date: dateValue.toString(), ...data[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> </script>
<div class="flex flex-row items-center space-x-2"> <div class="w-full max-w-2xl">
<h1 class="text-3xl mr-auto">{formatDate(entry.date)}</h1> <!-- 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} {#if !edit}
<button <Button variant="secondary" onclick={startEdit}>Edit</Button>
class="btn bg-secondary py-2 px-3 rounded-lg hover:opacity-60 transition-brightness"
onclick={() => (edit = !edit)}>Edit</button
>
{:else} {:else}
<button <Button
class="bg-destructive py-2 px-3 rounded-lg hover:opacity-60 transition-brightness" variant="destructive"
onclick={deleteEntry} onclick={handleDelete}
disabled={isLoading}
> >
delete Delete
</button> </Button>
<button <Button
class="bg-primary text-accent py-2 px-3 rounded-lg hover:opacity-60 transition-brightness" variant="default"
onclick={handleSave}
disabled={isLoading}
> >
save {isLoading ? "Saving..." : "Save"}
</button> </Button>
<button <Button
class="bg-secondary py-2 px-3 rounded-lg hover:opacity-60 transition-brightness" variant="outline"
onclick={() => (edit = false)} onclick={cancelEdit}
disabled={isLoading}
> >
cancel Cancel
</button> </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}
<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-96 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} {/if}
</div> </div>
<!-- Edit Mode -->
{:else}
<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-96 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

@@ -7,10 +7,4 @@
</script> </script>
{#if data.entry}
<Editor dateValue={data.date} /> <Editor dateValue={data.date} />
{:else}
No entry exists for {data.date}
{/if}

View File

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