Compare commits
15 Commits
8b4051e473
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7865fa2c2d | |||
| 29bb0e860a | |||
| c1af1e8ebb | |||
| 798f2c1e44 | |||
| 22772ad436 | |||
| cb027a6e92 | |||
| c1e39bbf86 | |||
| aa58dfe58b | |||
| ce2eabf1f5 | |||
| e934d4893a | |||
| dad55dab39 | |||
| fc7cd5a60a | |||
| e71acd2ce1 | |||
| 1fdf07cdf7 | |||
| 4f3eee5a49 |
@@ -10,6 +10,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
"@fullcalendar/core": "^6.1.20",
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
"@fullcalendar/interaction": "^6.1.20",
|
||||
"@fullcalendar/list": "^6.1.20",
|
||||
"@fullcalendar/resource": "^6.1.20",
|
||||
"@fullcalendar/scrollgrid": "^6.1.20",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.16.8",
|
||||
"dotenv": "^17.2.3",
|
||||
|
||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -11,6 +11,24 @@ importers:
|
||||
'@astrojs/node':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1(astro@5.16.8(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(tsx@4.21.0)(typescript@5.9.3))
|
||||
'@fullcalendar/core':
|
||||
specifier: ^6.1.20
|
||||
version: 6.1.20
|
||||
'@fullcalendar/daygrid':
|
||||
specifier: ^6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
'@fullcalendar/interaction':
|
||||
specifier: ^6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
'@fullcalendar/list':
|
||||
specifier: ^6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
'@fullcalendar/resource':
|
||||
specifier: ^6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
'@fullcalendar/scrollgrid':
|
||||
specifier: ^6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
|
||||
@@ -546,6 +564,39 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@fullcalendar/core@6.1.20':
|
||||
resolution: {integrity: sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==}
|
||||
|
||||
'@fullcalendar/daygrid@6.1.20':
|
||||
resolution: {integrity: sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@fullcalendar/interaction@6.1.20':
|
||||
resolution: {integrity: sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@fullcalendar/list@6.1.20':
|
||||
resolution: {integrity: sha512-7Hzkbb7uuSqrXwTyD0Ld/7SwWNxPD6SlU548vtkIpH55rZ4qquwtwYdMPgorHos5OynHA4OUrZNcH51CjrCf2g==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@fullcalendar/premium-common@6.1.20':
|
||||
resolution: {integrity: sha512-rT+AitNnRyZuFEtYvsB1OJ2g1Bq2jmTR6qdn/dEU6LwkIj/4L499goLtMOena/JyJ31VBztdHrccX//36QrY3w==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@fullcalendar/resource@6.1.20':
|
||||
resolution: {integrity: sha512-vpQs1eYJbc1zGOzF3obVVr+XsHTMTG7STKVQBEGy3AeFgfosRkUz+3DUawmy98vSjJUYOAQHO+pWW0ek0n5g0w==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@fullcalendar/scrollgrid@6.1.20':
|
||||
resolution: {integrity: sha512-M55m0hxpou4IPObto5f0nVcXvIj3rkSTba0ypclSFDwBz3JxuCPS6l8kaUznqlZCr2Ld/HFJr+jwyvY070AafQ==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@img/colour@1.0.0':
|
||||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1957,6 +2008,9 @@ packages:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
preact@10.12.1:
|
||||
resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==}
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2729,6 +2783,36 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@fullcalendar/core@6.1.20':
|
||||
dependencies:
|
||||
preact: 10.12.1
|
||||
|
||||
'@fullcalendar/daygrid@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
|
||||
'@fullcalendar/interaction@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
|
||||
'@fullcalendar/list@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
|
||||
'@fullcalendar/premium-common@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
|
||||
'@fullcalendar/resource@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
'@fullcalendar/premium-common': 6.1.20(@fullcalendar/core@6.1.20)
|
||||
|
||||
'@fullcalendar/scrollgrid@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
'@fullcalendar/premium-common': 6.1.20(@fullcalendar/core@6.1.20)
|
||||
|
||||
'@img/colour@1.0.0':
|
||||
optional: true
|
||||
|
||||
@@ -4281,6 +4365,8 @@ snapshots:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
preact@10.12.1: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
promise-limit@2.7.0:
|
||||
|
||||
33
src/component/calendar.astro
Normal file
33
src/component/calendar.astro
Normal file
@@ -0,0 +1,33 @@
|
||||
<div id="calendar" class="bg-[#009FB75f] p-4 rounded-xl"/>
|
||||
|
||||
<script>
|
||||
import { Calendar } from '@fullcalendar/core';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import listPlugin from '@fullcalendar/list';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import scrollgridPlugin from '@fullcalendar/scrollgrid';
|
||||
import resourcePlugin from '@fullcalendar/resource';
|
||||
|
||||
let calendarEl = document.getElementById('calendar')!;
|
||||
let calendar = new Calendar(calendarEl, {
|
||||
plugins: [ dayGridPlugin, listPlugin, interactionPlugin, scrollgridPlugin, resourcePlugin ],
|
||||
initialView: 'dayGridMonth',
|
||||
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
|
||||
headerToolbar: {
|
||||
left: 'prev,next',
|
||||
center: 'title',
|
||||
right: 'today'
|
||||
},
|
||||
selectable: true,
|
||||
|
||||
select: function(info) {
|
||||
window.location.href = `/?editor=${info.startStr}`
|
||||
},
|
||||
datesSet: function(info) {
|
||||
// Called when the date range changes (e.g., when navigating months)
|
||||
// console.log(info);
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
</script>
|
||||
15
src/component/core/head.astro
Normal file
15
src/component/core/head.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
|
||||
const {
|
||||
title
|
||||
} = Astro.props
|
||||
|
||||
---
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
21
src/component/core/layout.astro
Normal file
21
src/component/core/layout.astro
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import Head from "./head.astro";
|
||||
import Navbar from "./navbar.astro"
|
||||
|
||||
const {
|
||||
title = "Memento"
|
||||
} = Astro.props
|
||||
|
||||
---
|
||||
|
||||
<html>
|
||||
<Head
|
||||
title={title}
|
||||
/>
|
||||
|
||||
<body>
|
||||
<Navbar />
|
||||
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
3
src/component/core/navbar.astro
Normal file
3
src/component/core/navbar.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="flex w-full h-14 px-2 text-2xl rounded-xl mb-4 bg-[#009FB7] place-items-center">
|
||||
<p>Memento</p>
|
||||
</div>
|
||||
52
src/component/editor.astro
Normal file
52
src/component/editor.astro
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
const {
|
||||
entry,
|
||||
editMode
|
||||
} = Astro.props
|
||||
---
|
||||
|
||||
<!-- Share the data from the server -->
|
||||
<div id="data-entry" hidden>{entry}</div>
|
||||
<div id="data-editMode" hidden>{editMode}</div>
|
||||
|
||||
<script>
|
||||
import Quill from "quill";
|
||||
import { uploadEntry } from '../utils/quill'
|
||||
import type { Entry } from "../utils/quill";
|
||||
|
||||
const entry = JSON.parse(document.getElementById('data-entry')!.innerText)
|
||||
const editMode = document.getElementById('data-editMode')!.innerText === 'true' ? true : false
|
||||
|
||||
console.log(editMode)
|
||||
|
||||
const quill = new Quill('#editor', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
['image']
|
||||
],
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'bubble',
|
||||
readOnly: !editMode
|
||||
});
|
||||
|
||||
quill.setContents(entry.content)
|
||||
|
||||
document.querySelector("#upload")?.addEventListener('click', async () => {
|
||||
const contents = quill.getContents()
|
||||
|
||||
let entry: Entry = {
|
||||
content: contents,
|
||||
date: '2026-01-13T10:49:43Z',
|
||||
location: null
|
||||
}
|
||||
|
||||
await uploadEntry(entry)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<div class="bg-[#009FB75f] rounded-xl">
|
||||
<div id="editor" />
|
||||
</div>
|
||||
@@ -1,8 +1,8 @@
|
||||
import { integer, pgTable, varchar, date, json } from "drizzle-orm/pg-core";
|
||||
import { integer, pgTable, json, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
export const entryTable = pgTable("entries", {
|
||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
date: varchar().notNull(),
|
||||
date: timestamp({ mode: 'date', withTimezone: true }).notNull().defaultNow(),
|
||||
location: json(),
|
||||
content: json(),
|
||||
});
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { APIContext } from 'astro';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '../../../utils/db';
|
||||
import { entryTable } from '../../../db/schema';
|
||||
import { httpResponse } from '../../../utils/response';
|
||||
|
||||
export async function GET({ params }: APIContext) {
|
||||
try {
|
||||
const { id } = params
|
||||
if (!id) {
|
||||
return httpResponse({'error': 'no id provided'}, 400)
|
||||
}
|
||||
return getEntry(id)
|
||||
} catch (error) {
|
||||
return httpResponse({ error: `Failed to retrieve entry: ${error}` }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
async function getEntry(id) {
|
||||
try {
|
||||
const entry = await db.select().from(entryTable).where(eq(entryTable.id, id))
|
||||
if (entry.length == 0) {
|
||||
return httpResponse({'error': 'entry not found'}, 404)
|
||||
}
|
||||
return httpResponse(entry[0], 200)
|
||||
} catch {
|
||||
return httpResponse({'error': 'bad request'}, 400)
|
||||
}
|
||||
|
||||
}
|
||||
72
src/pages/api/entry/index.ts
Normal file
72
src/pages/api/entry/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { APIContext } from "astro";
|
||||
import { httpResponse } from "@util/response";
|
||||
import { getParams } from "@util/http";
|
||||
import { eq, like, sql } from 'drizzle-orm';
|
||||
import { db } from '@util/db';
|
||||
import { entryTable } from '@db/schema';
|
||||
|
||||
export async function GET({ request }: APIContext) {
|
||||
const { id, date, month } = getParams(request)
|
||||
|
||||
if (id && !isNaN(Number(id))) {
|
||||
return getEntryByID(Number(id))
|
||||
}
|
||||
|
||||
if (date) {
|
||||
return getEntryByDate(date)
|
||||
}
|
||||
|
||||
if (month) {
|
||||
return getEntryByMonth(month)
|
||||
}
|
||||
|
||||
return httpResponse({ error: 'Failed to retrieve entry' }, 500);
|
||||
}
|
||||
|
||||
async function getEntryByID(id: number) {
|
||||
try {
|
||||
const entry = await db.select().from(entryTable).where(eq(entryTable.id, id))
|
||||
if (entry.length == 0) {
|
||||
return httpResponse({'error': 'entry not found'}, 404)
|
||||
}
|
||||
return httpResponse(entry[0], 200)
|
||||
} catch {
|
||||
return httpResponse({'error': 'bad request'}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
async function getEntryByDate(dateString: string) {
|
||||
try {
|
||||
// timezones suck
|
||||
const startDate = new Date(dateString)
|
||||
startDate.setHours(0, 0, 0, 0)
|
||||
|
||||
const endDate = new Date(dateString)
|
||||
endDate.setHours(23, 59, 59, 999)
|
||||
|
||||
const entry = await db.select().from(entryTable).where(
|
||||
sql`${entryTable.date} >= ${startDate.toISOString()}::timestamp AND ${entryTable.date} <= ${endDate.toISOString()}::timestamp`
|
||||
)
|
||||
|
||||
return httpResponse(entry, 200)
|
||||
} catch(error) {
|
||||
return httpResponse({'error': error}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
async function getEntryByMonth(monthString: string) {
|
||||
try {
|
||||
const [year, month] = monthString.split('-').map(Number)
|
||||
|
||||
const startDate = new Date(year, month - 1, 1, 0, 0, 0, 0)
|
||||
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`
|
||||
)
|
||||
|
||||
return httpResponse(entries, 200)
|
||||
} catch(error) {
|
||||
return httpResponse({'error': error}, 400)
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,14 @@ export async function POST({ request }: APIContext) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const entry: typeof entryTable.$inferInsert = body
|
||||
entry.date = new Date(body.date)
|
||||
|
||||
await db.insert(entryTable).values(entry)
|
||||
|
||||
return httpResponse({ "success": entry }, 200);
|
||||
} catch(e) {
|
||||
return httpResponse({ "error": `Malformed JSON (${e})` }, 400)
|
||||
}
|
||||
|
||||
return httpResponse(null, 200);
|
||||
}
|
||||
|
||||
return httpResponse(null, 400);
|
||||
|
||||
@@ -1,61 +1,24 @@
|
||||
---
|
||||
import "../styles/global.css"
|
||||
import Layout from "../component/core/layout.astro"
|
||||
import Calendar from "../component/calendar.astro"
|
||||
import Editor from "../component/editor.astro"
|
||||
|
||||
|
||||
const res = await fetch(new URL('/api/entry?id=5', Astro.url))
|
||||
const en = await res.json()
|
||||
---
|
||||
|
||||
<script>
|
||||
import Quill from "quill";
|
||||
import { uploadEntry } from '../utils/quill'
|
||||
import type { Entry } from "../utils/quill";
|
||||
|
||||
const quill = new Quill('#editor', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
['image']
|
||||
],
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow', // or 'bubble'
|
||||
});
|
||||
|
||||
document.querySelector("#upload")?.addEventListener('click', async () => {
|
||||
const contents = quill.getContents()
|
||||
|
||||
let entry: Entry = {
|
||||
content: contents,
|
||||
date: '2026-01-13T10:49:43Z',
|
||||
location: null
|
||||
}
|
||||
|
||||
await uploadEntry(entry)
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Astro</h1>
|
||||
<div id="editor">
|
||||
<p>Hello World!</p>
|
||||
<p>Some initial <strong>bold</strong> text</p>
|
||||
<p><br /></p>
|
||||
<Layout>
|
||||
<div class="flex flex-col md:flex-row md:space-x-4 space-y-4 w-full">
|
||||
<div class="md:w-1/2">
|
||||
<Calendar />
|
||||
<div id="entry-list" />
|
||||
</div>
|
||||
|
||||
<button id="upload" class="mt-2">
|
||||
upload
|
||||
</button>
|
||||
|
||||
<div id="result">
|
||||
|
||||
<div id="right" class="md:w-1/2">
|
||||
<Editor entry={JSON.stringify(en)} editMode={false} />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -4,9 +4,15 @@
|
||||
@import 'quill/dist/quill.snow.css';
|
||||
|
||||
html {
|
||||
@apply p-6;
|
||||
@apply py-3 px-4;
|
||||
@apply bg-[#00171F] text-white
|
||||
}
|
||||
|
||||
html body {
|
||||
@apply font-[AmericanTypewriter];
|
||||
/* @apply w-full md:w-[68%] mx-auto h-full; */
|
||||
}
|
||||
|
||||
button {
|
||||
@apply py-2 px-3 bg-green-400 rounded-xl;
|
||||
}
|
||||
6
src/utils/http.ts
Normal file
6
src/utils/http.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function getParams(request: Request) {
|
||||
const params = request.url.split("?")[1]
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const paramsDict = Object.fromEntries(searchParams.entries());
|
||||
return paramsDict
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@component/*": ["src/components/*.astro"],
|
||||
"@layout/*": ["src/layout/*.astro"],
|
||||
"@util/*": ["src/utils/*"],
|
||||
"@db/*": ["src/db/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user