Compare commits
10 Commits
faed363820
...
1b0665222d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b0665222d | |||
| b8aad98b85 | |||
| 2fe5115e06 | |||
| 171af1e435 | |||
| 9e521b81aa | |||
| a2d13dc4c8 | |||
| 4ccd12a9dc | |||
| d14f3dfdb6 | |||
| 59e117003f | |||
| 2e85fef7e2 |
@@ -11,11 +11,18 @@
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<button
|
||||
class="bg-white/20 p-4 rounded-xl w-full hover:brightness-80 transition-all"
|
||||
class="bg-white/10 p-4 rounded-xl w-full hover:brightness-80 transition-all"
|
||||
onclick={() => edit = true}
|
||||
>
|
||||
<p class="rounded-full bg-white/30 p-2 h-10 w-10">+</p>
|
||||
<p>Add Entry for {date}</p>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="rounded-full bg-white/20 p-3 h-14 w-14 flex items-center justify-center">
|
||||
<span class="text-2xl font-light text-white">+</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-white/90 font-medium text-lg">Add Entry</p>
|
||||
<p class="text-white/60 text-sm mt-1">{date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -27,9 +27,13 @@
|
||||
let currentYear = $state(parseInt(year));
|
||||
let currentMonth = $state(parseInt(month));
|
||||
|
||||
function toISODate(year: number, month: number, date: number) {
|
||||
return `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function getEntryForDay(year: number, month: number, date: number) {
|
||||
if (!entries) return undefined;
|
||||
const dateStr = new Date(year, month, date).toISOString().split("T")[0];
|
||||
const dateStr = toISODate(year, month, date);
|
||||
return entries.find((e) => e.date.split("T")[0] === dateStr);
|
||||
}
|
||||
|
||||
@@ -44,7 +48,7 @@
|
||||
date: date.getDate(),
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear(),
|
||||
iso: date.toISOString().split('T')[0],
|
||||
iso: toISODate(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||
dayName: headers[date.getDay() === 0 ? 6 : date.getDay() - 1],
|
||||
isCurrentMonth,
|
||||
isToday: date.toDateString() === today.toDateString(),
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<style>
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*#img-upload-button {
|
||||
#img-upload-button {
|
||||
@apply text-center bg-white/30 h-60 w-full rounded-2xl block hover:text-white/30 transition-all;
|
||||
}*/
|
||||
}
|
||||
</style>
|
||||
@@ -2,27 +2,68 @@
|
||||
import ImageTarget from "./imageTarget.svelte";
|
||||
import TextEditor from "./textEditor.svelte";
|
||||
import { formatDate } from "$lib/date.ts";
|
||||
import { createEntry } from "$lib/upload.ts";
|
||||
import { createEntry, updateEntry } from "$lib/upload.ts";
|
||||
|
||||
let { newEntryDate, entry = $bindable(), edit = $bindable(false) } = $props();
|
||||
|
||||
let newEntry = $state({ date: newEntryDate, image: "", content: "" });
|
||||
|
||||
let isNewEntry = !entry && newEntryDate
|
||||
|
||||
let newEntry = $state({
|
||||
date: isNewEntry ? newEntryDate : entry.date,
|
||||
image: isNewEntry ? "" : entry.image,
|
||||
content: isNewEntry ? "" : entry.content
|
||||
});
|
||||
|
||||
|
||||
async function saveEntry() {
|
||||
if (isNewEntry) {
|
||||
await createEntry(newEntry);
|
||||
} else {
|
||||
await updateEntry(newEntry);
|
||||
}
|
||||
edit = false;
|
||||
}
|
||||
|
||||
async function deleteEntry() {
|
||||
const res = 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');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row mb-2">
|
||||
<p class="text-3xl font-bold">{entry ? formatDate(entry.date) : formatDate(newEntryDate)}</p>
|
||||
<p class="text-3xl font-bold mr-auto">{entry ? formatDate(entry.date) : formatDate(newEntryDate)}</p>
|
||||
|
||||
{#if edit}
|
||||
|
||||
{#if !isNewEntry}
|
||||
<button
|
||||
class="bg-red-500 px-3 rounded-lg"
|
||||
onclick={deleteEntry}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="bg-white/10 px-3 rounded-lg ml-auto"
|
||||
class="bg-white/10 px-3 rounded-lg ml-1"
|
||||
onclick={() => edit = false}
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
<button
|
||||
class="bg-white/20 px-3 rounded-lg ml-1"
|
||||
onclick={() => { createEntry(newEntry); edit = false }}
|
||||
onclick={saveEntry}
|
||||
>
|
||||
done
|
||||
</button>
|
||||
@@ -37,10 +78,5 @@
|
||||
|
||||
</div>
|
||||
|
||||
{#if entry}
|
||||
<ImageTarget bind:image={entry.image} bind:edit />
|
||||
<TextEditor bind:content={entry.content} bind:edit />
|
||||
{:else}
|
||||
<ImageTarget bind:image={newEntry.image} bind:edit />
|
||||
<TextEditor bind:content={newEntry.content} bind:edit />
|
||||
{/if}
|
||||
<ImageTarget bind:image={newEntry.image} bind:edit />
|
||||
<TextEditor bind:content={newEntry.content} bind:edit />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { marked } from "marked";
|
||||
</script>
|
||||
|
||||
<div class="bg-white/20 min-h-30 rounded-2xl p-2 mt-4">
|
||||
<div class="min-h-30 bg-white/10 p-2 rounded-2xl w-full">
|
||||
{#if edit}
|
||||
<textarea
|
||||
class="w-full min-h-30 border-none bg-white/10 rounded-xl p-2"
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
const formattedDate = formatDate(entry.date);
|
||||
</script>
|
||||
|
||||
<li class="bg-white/15 rounded-2xl mb-4 p-3" onclick={clickCB}>
|
||||
<button class="bg-white/10 p-4 rounded-xl w-full hover:brightness-80 transition-all mb-3" onclick={clickCB}>
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<div class="text-start">
|
||||
<p class="text-sm text-white/60">{formattedDate}</p>
|
||||
{@html marked(entry.content)}
|
||||
</div>
|
||||
<img
|
||||
class="max-h-30 min-h-15 max-w-1/2 rounded-xl object-cover ml-auto"
|
||||
src={entry.image}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</button>
|
||||
|
||||
@@ -39,4 +39,19 @@ export async function createEntry(newEntry) {
|
||||
},
|
||||
body: JSON.stringify(newEntry),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateEntry(entry) {
|
||||
if (entry.image) {
|
||||
const url = await uploadImage(entry.image)
|
||||
entry.image = url
|
||||
}
|
||||
|
||||
await fetch("/api/entry/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(entry),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
|
||||
<Header />
|
||||
|
||||
{@render children()}
|
||||
<div class="md:max-w-2/3 mx-auto">
|
||||
<Header />
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
<script lang="ts">
|
||||
const {
|
||||
data
|
||||
} = $props()
|
||||
|
||||
import Calendar from '$lib/components/calendar.svelte'
|
||||
import Editor from '$lib/components/editor/index.svelte';
|
||||
import EntrySummaryView from '$lib/components/entrySummaryView.svelte'
|
||||
import AddEntryCover from '$lib/components/addEntryCover.svelte'
|
||||
import { today } from '$lib/date';
|
||||
|
||||
let edit = $state(false)
|
||||
let selectedEntry = $state('')
|
||||
let selectedDate = $state(today())
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
import Calendar from "$lib/components/calendar.svelte";
|
||||
import Editor from "$lib/components/editor/index.svelte";
|
||||
import EntrySummaryView from "$lib/components/entrySummaryView.svelte";
|
||||
import AddEntryCover from "$lib/components/addEntryCover.svelte";
|
||||
import { today } from "$lib/date";
|
||||
|
||||
let edit = $state(false);
|
||||
let selectedEntry = $state("");
|
||||
let selectedDate = $state(today());
|
||||
|
||||
let selectEntry = async (hasEntry: boolean, data: string | null) => {
|
||||
// data should be the entryID if hasEntry == true, or the date if there is no entry
|
||||
if (hasEntry) {
|
||||
const res = await fetch(`/api/entry?id=${data}`)
|
||||
selectedEntry = await res.json()
|
||||
selectedDate = selectedEntry.date
|
||||
} else {
|
||||
selectedEntry = ''
|
||||
selectedDate = data as string
|
||||
}
|
||||
}
|
||||
|
||||
// data should be the entryID if hasEntry == true, or the date if there is no entry
|
||||
if (hasEntry) {
|
||||
const res = await fetch(`/api/entry?id=${data}`);
|
||||
selectedEntry = await res.json();
|
||||
selectedDate = selectedEntry.date;
|
||||
} else {
|
||||
selectedEntry = "";
|
||||
selectedDate = data as string;
|
||||
}
|
||||
};
|
||||
|
||||
// $effect(() => {selectedEntry = ''})
|
||||
</script>
|
||||
|
||||
<Calendar entries={data.all} dayClickCallback={selectEntry} />
|
||||
|
||||
<div class="flex flex-col md:flex-row space-y-4 w-full mt-6">
|
||||
|
||||
<div class="md:w-1/2 md:order-2">
|
||||
{#if selectedEntry}
|
||||
<Editor bind:entry={selectedEntry} />
|
||||
{#key selectedEntry.id}
|
||||
<Editor bind:entry={selectedEntry} />
|
||||
{/key}
|
||||
{:else}
|
||||
<AddEntryCover bind:date={selectedDate} />
|
||||
{#key selectedDate}
|
||||
<AddEntryCover bind:date={selectedDate} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="md:w-1/2 md:mr-4">
|
||||
<ul>
|
||||
{#each data.all as entry}
|
||||
<EntrySummaryView clickCB={async () => await selectEntry(true, entry.id)} {entry} />
|
||||
<EntrySummaryView
|
||||
clickCB={async () => await selectEntry(true, entry.id)}
|
||||
{entry}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
16
src/routes/api/entry/delete/+server.ts
Normal file
16
src/routes/api/entry/delete/+server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { db } from "$lib/server/db";
|
||||
import { entryTable } from "$lib/server/db/schema";
|
||||
import { httpResponse } from "$lib/server/http";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { id } = await request.json();
|
||||
|
||||
try {
|
||||
await db.delete(entryTable).where(eq(entryTable.id, id)).execute();
|
||||
|
||||
return httpResponse({ message: "Entry deleted successfully" }, 200);
|
||||
} catch (error) {
|
||||
return httpResponse({ message: "Failed to delete entry", details: error }, 500);
|
||||
}
|
||||
}
|
||||
16
src/routes/api/entry/update/+server.ts
Normal file
16
src/routes/api/entry/update/+server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { db } from "$lib/server/db";
|
||||
import { entryTable } from "$lib/server/db/schema";
|
||||
import { httpResponse } from "$lib/server/http";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { id, content, image } = await request.json();
|
||||
|
||||
try {
|
||||
await db.update(entryTable).set({ content, image }).where(eq(entryTable.id, id)).execute();
|
||||
|
||||
return httpResponse({ message: "Entry updated successfully" }, 200);
|
||||
} catch (error) {
|
||||
return httpResponse({ message: "Failed to update entry", details: error }, 500);
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,25 @@ async function readImage(id: string) {
|
||||
'Content-Type': contentType,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function DELETE({ params }) {
|
||||
try {
|
||||
const { id } = params
|
||||
await deleteImage(id!)
|
||||
return httpResponse({ message: 'Image deleted successfully' }, 200);
|
||||
} catch (error) {
|
||||
return httpResponse({ error: `Failed to delete image: ${error}` }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteImage(id: string) {
|
||||
|
||||
const filepath = join(UPLOAD_DIR, id);
|
||||
|
||||
try {
|
||||
await fs.unlink(filepath);
|
||||
} catch {
|
||||
throw new Error('Failed to delete image');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user