Push Preview Gong Notes
This commit is contained in:
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["svelte.svelte-vscode"]
|
"recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
- Non modificare mai i file direttamente, proponi la soluzione nella chat.
|
- Puoi modificare direttamente i tag e lo stile di componenti e pagine.
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -21,6 +21,8 @@
|
|||||||
"better-auth": "~1.4.21",
|
"better-auth": "~1.4.21",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-kit": "^0.31.8",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
"svelte": "^5.51.0",
|
"svelte": "^5.51.0",
|
||||||
"svelte-check": "^4.4.2",
|
"svelte-check": "^4.4.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@@ -4658,6 +4660,17 @@
|
|||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier-plugin-svelte": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
|
||||||
|
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-warning": {
|
"node_modules/process-warning": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes"
|
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes",
|
||||||
|
"lint": "prettier --check .",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@better-auth/cli": "~1.4.21",
|
"@better-auth/cli": "~1.4.21",
|
||||||
@@ -26,6 +28,8 @@
|
|||||||
"better-auth": "~1.4.21",
|
"better-auth": "~1.4.21",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-kit": "^0.31.8",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
"svelte": "^5.51.0",
|
"svelte": "^5.51.0",
|
||||||
"svelte-check": "^4.4.2",
|
"svelte-check": "^4.4.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
|||||||
5
src/app.d.ts
vendored
5
src/app.d.ts
vendored
@@ -4,7 +4,10 @@ import type { User, Session } from 'better-auth/minimal';
|
|||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface Locals { user?: User; session?: Session }
|
interface Locals {
|
||||||
|
user?: User;
|
||||||
|
session?: Session;
|
||||||
|
}
|
||||||
|
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
|
||||||
|
/>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { auth } from '$lib/server/auth';
|
|||||||
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||||
import { sequence } from '@sveltejs/kit/hooks';
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
||||||
const session = await auth.api.getSession({ headers: event.request.headers });
|
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||||
|
|
||||||
@@ -29,6 +27,6 @@ const handleRouting: Handle = async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const handle: Handle = sequence(handleBetterAuth, handleRouting);
|
export const handle: Handle = sequence(handleBetterAuth, handleRouting);
|
||||||
|
|||||||
2
src/lib/assets/composer/palette.svg
Normal file
2
src/lib/assets/composer/palette.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.022,23a11.053,11.053,0,0,0,10.921-9.5,5.853,5.853,0,0,0-.577-3.5c-1.655-3.146-4.777-2.671-7.056-2.322-1.18.178-2.4.366-2.865-.035A2.416,2.416,0,0,1,12.02,6c0-2.683,0-5-3-5C3.753,1,1,6.534,1,12A11.023,11.023,0,0,0,12.022,23ZM9.016,3c.909,0,1,0,1,3a3.941,3.941,0,0,0,1.122,3.168c1.163,1,2.844.741,4.469.494,2.483-.379,4.061-.482,4.986,1.276a3.844,3.844,0,0,1,.363,2.293A9.024,9.024,0,0,1,3,12C3,8.382,4.6,3,9.016,3ZM5,7.5A1.5,1.5,0,1,1,6.5,9,1.5,1.5,0,0,1,5,7.5ZM4,12a1.5,1.5,0,1,1,1.5,1.5A1.5,1.5,0,0,1,4,12Zm3.5,3A1.5,1.5,0,1,1,6,16.5,1.5,1.5,0,0,1,7.5,15Zm8,3A3.5,3.5,0,1,0,12,14.5,3.5,3.5,0,0,0,15.5,18Zm0-5A1.5,1.5,0,1,1,14,14.5,1.5,1.5,0,0,1,15.5,13Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 899 B |
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { FileAttachment, ImageAttachment, Note } from "$lib/editor";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import imageSvg from "$lib/assets/composer/image.svg"
|
|
||||||
import fileSvg from "$lib/assets/composer/file.svg"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
note?: Note;
|
|
||||||
onSubmit: (note: Note) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { note, onSubmit } : Props = $props();
|
|
||||||
|
|
||||||
const palette = ["#ffffff", "#fef3c7", "#dbeafe", "#dcfce7", "#fee2e2", "#f3e8ff"];
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!note) {
|
|
||||||
note = {
|
|
||||||
id: nanoid(),
|
|
||||||
title: "",
|
|
||||||
content: "",
|
|
||||||
color: palette[0],
|
|
||||||
date: new Date(),
|
|
||||||
images: [],
|
|
||||||
files: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Note } from "$lib/editor";
|
|
||||||
import NoteEditor from "./NoteEditor.svelte";
|
|
||||||
|
|
||||||
let expanded = $state(false);
|
|
||||||
|
|
||||||
const onCreate = (note: Note) => {
|
|
||||||
console.log("create note")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window
|
|
||||||
onkeydown={(e) => {
|
|
||||||
if (e.key === "n" && e.metaKey) {
|
|
||||||
expanded = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if( e.key === "Escape") {
|
|
||||||
expanded = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if !expanded}
|
|
||||||
<div>Scrivi una nota...</div>
|
|
||||||
{:else}
|
|
||||||
<div class="note-editor-container">
|
|
||||||
<NoteEditor note={undefined} onSubmit={onCreate} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
61
src/lib/components/board/BoardCanvas.svelte
Normal file
61
src/lib/components/board/BoardCanvas.svelte
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import NoteCard from '$lib/components/board/Note.svelte';
|
||||||
|
import type { BoardNote } from '$lib/components/board/script/types';
|
||||||
|
import { BOARD_GRID_SIZE, BOARD_HEIGHT, BOARD_WIDTH } from '$lib/components/board/script/constants';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
notes: BoardNote[];
|
||||||
|
onStartPan: (event: PointerEvent) => void;
|
||||||
|
onStartDrag: (event: PointerEvent, id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { notes, onStartPan, onStartDrag }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="board"
|
||||||
|
role="application"
|
||||||
|
aria-label="Bacheca note"
|
||||||
|
onpointerdown={onStartPan}
|
||||||
|
style={`--board-width:${BOARD_WIDTH}px; --board-height:${BOARD_HEIGHT}px; --grid-size:${BOARD_GRID_SIZE}px;`}
|
||||||
|
>
|
||||||
|
{#each notes as note (note.id)}
|
||||||
|
<div
|
||||||
|
class="note-item"
|
||||||
|
style={`left:${note.x}px; top:${note.y}px; z-index:${note.z};`}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onpointerdown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onStartDrag(event, note.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NoteCard {note} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.board {
|
||||||
|
position: relative;
|
||||||
|
width: var(--board-width);
|
||||||
|
height: var(--board-height);
|
||||||
|
padding: 90px 30px 60px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(140, 155, 176, 0.14) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(140, 155, 176, 0.14) 1px, transparent 1px);
|
||||||
|
background-size: var(--grid-size) var(--grid-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item {
|
||||||
|
position: absolute;
|
||||||
|
width: 280px;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
src/lib/components/board/Note.svelte
Normal file
65
src/lib/components/board/Note.svelte
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Note } from '$lib/components/editor/script/editor';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
note: Note;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { note }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article class="note-card" style={`background:${note.color};`}>
|
||||||
|
{#if note.title.trim().length > 0}
|
||||||
|
<h3 class="note-title">{note.title}</h3>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if note.content.trim().length > 0}
|
||||||
|
<p class="note-content">{note.content}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="note-content empty">Nota senza contenuto</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="note-date">{note.date.toLocaleString('it-IT')}</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.note-card {
|
||||||
|
width: 280px;
|
||||||
|
min-height: 140px;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 1px 2px rgba(60, 64, 67, 0.2), 0 2px 6px rgba(60, 64, 67, 0.18);
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
color: #202124;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-content {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.42rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-content.empty {
|
||||||
|
color: #5f6368;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-date {
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
src/lib/components/board/script/constants.ts
Normal file
9
src/lib/components/board/script/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const BOARD_WIDTH = 5400;
|
||||||
|
export const BOARD_HEIGHT = 5400;
|
||||||
|
|
||||||
|
export const BOARD_GRID_SIZE = 16;
|
||||||
|
|
||||||
|
export const NOTE_CARD_WIDTH = 280;
|
||||||
|
export const NOTE_CARD_MIN_HEIGHT = 140;
|
||||||
|
|
||||||
|
export const MINIMAP_WIDTH = 220;
|
||||||
31
src/lib/components/board/script/types.ts
Normal file
31
src/lib/components/board/script/types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Note } from '$lib/components/editor/script/editor';
|
||||||
|
|
||||||
|
export type BoardNote = Note & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DragState =
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export type PanState =
|
||||||
|
| {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
scrollLeft: number;
|
||||||
|
scrollTop: number;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export type ViewportState = {
|
||||||
|
scrollLeft: number;
|
||||||
|
scrollTop: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
18
src/lib/components/board/script/utils.ts
Normal file
18
src/lib/components/board/script/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
BOARD_HEIGHT,
|
||||||
|
BOARD_WIDTH,
|
||||||
|
NOTE_CARD_MIN_HEIGHT,
|
||||||
|
NOTE_CARD_WIDTH
|
||||||
|
} from '$lib/components/board/script/constants';
|
||||||
|
|
||||||
|
export const clamp = (value: number, min: number, max: number) =>
|
||||||
|
Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
export const getRandomPosition = (idx: number) => ({
|
||||||
|
x: 200 + ((idx * 420) % (BOARD_WIDTH - NOTE_CARD_WIDTH - 260)),
|
||||||
|
y: 140 + Math.floor(idx / 8) * 260
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clampNoteX = (x: number) => clamp(x, 0, BOARD_WIDTH - NOTE_CARD_WIDTH);
|
||||||
|
|
||||||
|
export const clampNoteY = (y: number) => clamp(y, 0, BOARD_HEIGHT - NOTE_CARD_MIN_HEIGHT);
|
||||||
0
src/lib/components/editor/ColorSelector.svelte
Normal file
0
src/lib/components/editor/ColorSelector.svelte
Normal file
193
src/lib/components/editor/NoteEditor.svelte
Normal file
193
src/lib/components/editor/NoteEditor.svelte
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { FileAttachment, ImageAttachment, Note } from '$lib/components/editor/script/editor';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import imageSvg from '$lib/assets/composer/image.svg';
|
||||||
|
import fileSvg from '$lib/assets/composer/file.svg';
|
||||||
|
import paletteSvg from '$lib/assets/composer/palette.svg';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
note?: Note;
|
||||||
|
onSubmit: (note: Note) => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { note, onSubmit, onClose }: Props = $props();
|
||||||
|
|
||||||
|
const palette = ['#ffffff', '#fef3c7', '#dbeafe', '#dcfce7', '#fee2e2', '#f3e8ff'];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!note) {
|
||||||
|
note = {
|
||||||
|
id: nanoid(),
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
color: palette[0],
|
||||||
|
date: new Date(),
|
||||||
|
images: [],
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (note) {
|
||||||
|
const hasContent = note.title.trim().length > 0 || note.content.trim().length > 0;
|
||||||
|
if (hasContent) {
|
||||||
|
onSubmit({ ...note, date: new Date() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window />
|
||||||
|
|
||||||
|
{#if note}
|
||||||
|
<section class="note-editor" style={`background: ${note.color};`}>
|
||||||
|
<input
|
||||||
|
class="title-input"
|
||||||
|
type="text"
|
||||||
|
bind:value={note.title}
|
||||||
|
placeholder="Titolo"
|
||||||
|
aria-label="Titolo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
class="content-input"
|
||||||
|
bind:value={note.content}
|
||||||
|
placeholder="Scrivi una nota..."
|
||||||
|
aria-label="Contenuto nota"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<footer class="editor-footer">
|
||||||
|
<div class="left-actions" aria-label="Azioni nota">
|
||||||
|
<button type="button" class="icon-btn" aria-label="Sfondo colore">
|
||||||
|
<img src={paletteSvg} alt="" class="icon-svg" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="icon-btn" aria-label="Immagine">
|
||||||
|
<img src={imageSvg} alt="" class="icon-svg" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="icon-btn" aria-label="File">
|
||||||
|
<img src={fileSvg} alt="" class="icon-svg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-actions">
|
||||||
|
<button type="button" class="close-btn" onclick={handleClose}>Chiudi</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.note-editor {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(60, 64, 67, 0.3), 0 4px 8px rgba(60, 64, 67, 0.15);
|
||||||
|
padding: 10px 14px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input,
|
||||||
|
.content-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #202124;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input:focus,
|
||||||
|
.content-input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input {
|
||||||
|
min-height: 36px;
|
||||||
|
padding-right: 34px;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input::placeholder {
|
||||||
|
color: #3c4043;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-input {
|
||||||
|
min-height: 110px;
|
||||||
|
resize: none;
|
||||||
|
padding: 2px 0 8px;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-input::placeholder {
|
||||||
|
color: #5f6368;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: #5f6368;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background: #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #202124;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #f1f3f4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
src/lib/components/editor/script/types.ts
Normal file
23
src/lib/components/editor/script/types.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export type Note = {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
color: string;
|
||||||
|
date: Date;
|
||||||
|
|
||||||
|
images: ImageAttachment[];
|
||||||
|
files: FileAttachment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageAttachment = NoteAttachment & { kind: 'image' };
|
||||||
|
export type FileAttachment = NoteAttachment & { kind: 'file' };
|
||||||
|
|
||||||
|
interface NoteAttachment {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
mimeType: string;
|
||||||
|
previewUrl?: string;
|
||||||
|
}
|
||||||
0
src/lib/components/editor/script/utils.ts
Normal file
0
src/lib/components/editor/script/utils.ts
Normal file
112
src/lib/components/ui/Minimap.svelte
Normal file
112
src/lib/components/ui/Minimap.svelte
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { BOARD_HEIGHT, BOARD_WIDTH, MINIMAP_WIDTH } from '$lib/components/board/script/constants';
|
||||||
|
import type { BoardNote, ViewportState } from '$lib/components/board/script/types';
|
||||||
|
import { clamp } from '$lib/components/board/script/utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
notes: BoardNote[];
|
||||||
|
viewport: ViewportState;
|
||||||
|
onNavigate: (scrollLeft: number, scrollTop: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { notes, viewport, onNavigate }: Props = $props();
|
||||||
|
|
||||||
|
let dragging = $state(false);
|
||||||
|
|
||||||
|
const minimapHeight = (MINIMAP_WIDTH * BOARD_HEIGHT) / BOARD_WIDTH;
|
||||||
|
|
||||||
|
const viewportLeftPercent = $derived((viewport.scrollLeft / BOARD_WIDTH) * 100);
|
||||||
|
const viewportTopPercent = $derived((viewport.scrollTop / BOARD_HEIGHT) * 100);
|
||||||
|
const viewportWidthPercent = $derived((viewport.width / BOARD_WIDTH) * 100);
|
||||||
|
const viewportHeightPercent = $derived((viewport.height / BOARD_HEIGHT) * 100);
|
||||||
|
|
||||||
|
const navigateFromPointer = (event: PointerEvent) => {
|
||||||
|
const element = event.currentTarget as HTMLElement;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
const relX = clamp(event.clientX - rect.left, 0, rect.width);
|
||||||
|
const relY = clamp(event.clientY - rect.top, 0, rect.height);
|
||||||
|
|
||||||
|
const nextLeft = (relX / rect.width) * BOARD_WIDTH - viewport.width / 2;
|
||||||
|
const nextTop = (relY / rect.height) * BOARD_HEIGHT - viewport.height / 2;
|
||||||
|
|
||||||
|
onNavigate(nextLeft, nextTop);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class="minimap-shell" aria-label="Minimappa bacheca">
|
||||||
|
<div
|
||||||
|
class="minimap"
|
||||||
|
role="application"
|
||||||
|
aria-label="Minimappa interattiva"
|
||||||
|
style={`--minimap-width:${MINIMAP_WIDTH}px; --minimap-height:${minimapHeight}px;`}
|
||||||
|
onpointerdown={(event) => {
|
||||||
|
dragging = true;
|
||||||
|
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||||
|
navigateFromPointer(event);
|
||||||
|
}}
|
||||||
|
onpointermove={(event) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
navigateFromPointer(event);
|
||||||
|
}}
|
||||||
|
onpointerup={() => (dragging = false)}
|
||||||
|
onpointercancel={() => (dragging = false)}
|
||||||
|
>
|
||||||
|
{#each notes as note (note.id)}
|
||||||
|
<span
|
||||||
|
class="note-dot"
|
||||||
|
style={`left:${(note.x / BOARD_WIDTH) * 100}%; top:${(note.y / BOARD_HEIGHT) * 100}%;`}
|
||||||
|
></span>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="viewport-frame"
|
||||||
|
style={`left:${viewportLeftPercent}%; top:${viewportTopPercent}%; width:${viewportWidthPercent}%; height:${viewportHeightPercent}%;`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.minimap-shell {
|
||||||
|
position: fixed;
|
||||||
|
left: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
z-index: 90;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.68);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
box-shadow: 0 4px 14px rgba(32, 33, 36, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap {
|
||||||
|
position: relative;
|
||||||
|
width: var(--minimap-width);
|
||||||
|
height: var(--minimap-height);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(140, 155, 176, 0.14) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(140, 155, 176, 0.14) 1px, transparent 1px);
|
||||||
|
background-size: 10px 10px;
|
||||||
|
background-color: rgba(234, 239, 245, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-dot {
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(50, 84, 122, 0.76);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-frame {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid rgba(17, 80, 145, 0.9);
|
||||||
|
background: rgba(63, 134, 206, 0.12);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.75);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
62
src/lib/components/ui/Navbar.svelte
Normal file
62
src/lib/components/ui/Navbar.svelte
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Note } from '$lib/editor';
|
||||||
|
import NoteEditor from '../editor/NoteEditor.svelte';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCreate: (note: Note) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { onCreate }: Props = $props();
|
||||||
|
|
||||||
|
let expanded = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
expanded = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if !expanded}
|
||||||
|
<button class="note-navbar-trigger" type="button" onclick={() => (expanded = true)}>
|
||||||
|
Scrivi una nota...
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="note-editor-container">
|
||||||
|
<NoteEditor note={undefined} onSubmit={onCreate} onClose={() => (expanded = false)} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.note-navbar-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 52px;
|
||||||
|
border: 1px solid #d3d3d3;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 0 18px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.95rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #5f6368;
|
||||||
|
cursor: text;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-navbar-trigger:hover,
|
||||||
|
.note-navbar-trigger:focus-visible {
|
||||||
|
border-color: #c6c6c6;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export type Note = {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
color: string;
|
|
||||||
date: Date;
|
|
||||||
|
|
||||||
images: ImageAttachment[];
|
|
||||||
files: FileAttachment[];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type ImageAttachment = NoteAttachment & { kind: "image" };
|
|
||||||
export type FileAttachment = NoteAttachment & { kind: "file" };
|
|
||||||
|
|
||||||
interface NoteAttachment {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
mimeType: string;
|
|
||||||
previewUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export type MediaProvider = 'youtube' | 'youtube-music' | 'spotify';
|
|
||||||
@@ -1,107 +1,105 @@
|
|||||||
import { relations, sql } from "drizzle-orm";
|
import { relations, sql } from 'drizzle-orm';
|
||||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
export const user = sqliteTable("user", {
|
export const user = sqliteTable('user', {
|
||||||
id: text("id").primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text('name').notNull(),
|
||||||
email: text("email").notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
emailVerified: integer("email_verified", { mode: "boolean" })
|
emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
|
||||||
.default(false)
|
image: text('image'),
|
||||||
.notNull(),
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||||
image: text("image"),
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
.notNull(),
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||||
.notNull(),
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.notNull()
|
||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
||||||
.notNull(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const session = sqliteTable(
|
export const session = sqliteTable(
|
||||||
"session",
|
'session',
|
||||||
{
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||||
token: text("token").notNull().unique(),
|
token: text('token').notNull().unique(),
|
||||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.notNull(),
|
.notNull(),
|
||||||
ipAddress: text("ip_address"),
|
ipAddress: text('ip_address'),
|
||||||
userAgent: text("user_agent"),
|
userAgent: text('user_agent'),
|
||||||
userId: text("user_id")
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: 'cascade' })
|
||||||
},
|
},
|
||||||
(table) => [index("session_userId_idx").on(table.userId)],
|
(table) => [index('session_userId_idx').on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const account = sqliteTable(
|
export const account = sqliteTable(
|
||||||
"account",
|
'account',
|
||||||
{
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
accountId: text("account_id").notNull(),
|
accountId: text('account_id').notNull(),
|
||||||
providerId: text("provider_id").notNull(),
|
providerId: text('provider_id').notNull(),
|
||||||
userId: text("user_id")
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: 'cascade' }),
|
||||||
accessToken: text("access_token"),
|
accessToken: text('access_token'),
|
||||||
refreshToken: text("refresh_token"),
|
refreshToken: text('refresh_token'),
|
||||||
idToken: text("id_token"),
|
idToken: text('id_token'),
|
||||||
accessTokenExpiresAt: integer("access_token_expires_at", {
|
accessTokenExpiresAt: integer('access_token_expires_at', {
|
||||||
mode: "timestamp_ms",
|
mode: 'timestamp_ms'
|
||||||
}),
|
}),
|
||||||
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
refreshTokenExpiresAt: integer('refresh_token_expires_at', {
|
||||||
mode: "timestamp_ms",
|
mode: 'timestamp_ms'
|
||||||
}),
|
}),
|
||||||
scope: text("scope"),
|
scope: text('scope'),
|
||||||
password: text("password"),
|
password: text('password'),
|
||||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.notNull(),
|
.notNull()
|
||||||
},
|
},
|
||||||
(table) => [index("account_userId_idx").on(table.userId)],
|
(table) => [index('account_userId_idx').on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const verification = sqliteTable(
|
export const verification = sqliteTable(
|
||||||
"verification",
|
'verification',
|
||||||
{
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
identifier: text("identifier").notNull(),
|
identifier: text('identifier').notNull(),
|
||||||
value: text("value").notNull(),
|
value: text('value').notNull(),
|
||||||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.notNull(),
|
.notNull()
|
||||||
},
|
},
|
||||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
(table) => [index('verification_identifier_idx').on(table.identifier)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const userRelations = relations(user, ({ many }) => ({
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
sessions: many(session),
|
sessions: many(session),
|
||||||
accounts: many(account),
|
accounts: many(account)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const sessionRelations = relations(session, ({ one }) => ({
|
export const sessionRelations = relations(session, ({ one }) => ({
|
||||||
user: one(user, {
|
user: one(user, {
|
||||||
fields: [session.userId],
|
fields: [session.userId],
|
||||||
references: [user.id],
|
references: [user.id]
|
||||||
}),
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const accountRelations = relations(account, ({ one }) => ({
|
export const accountRelations = relations(account, ({ one }) => ({
|
||||||
user: one(user, {
|
user: one(user, {
|
||||||
fields: [account.userId],
|
fields: [account.userId],
|
||||||
references: [user.id],
|
references: [user.id]
|
||||||
}),
|
})
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||||
import { user } from './auth.schema';
|
import { user } from './auth.schema';
|
||||||
|
|
||||||
export * from './auth.schema';
|
export * from './auth.schema';
|
||||||
|
|
||||||
export const notes = sqliteTable('notes', {
|
export const notes = sqliteTable('notes', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
userId: text('user_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: 'cascade' }),
|
||||||
|
|
||||||
title: text('title').notNull(),
|
title: text('title').notNull(),
|
||||||
content: text('text').notNull(),
|
content: text('text').notNull(),
|
||||||
|
|
||||||
x: real("x").default(0).notNull(),
|
x: real('x').default(0).notNull(),
|
||||||
y: real("y").default(0).notNull(),
|
y: real('y').default(0).notNull(),
|
||||||
z: integer("z").default(1).notNull(),
|
z: integer('z').default(1).notNull(),
|
||||||
|
|
||||||
color: text("color").default("#ffffff").notNull(),
|
color: text('color').default('#ffffff').notNull(),
|
||||||
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const noteFiles = sqliteTable('note_files', {
|
export const noteFiles = sqliteTable('note_files', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
noteId: text('note_id').notNull().references(() => notes.id, { onDelete: 'cascade' }),
|
noteId: text('note_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => notes.id, { onDelete: 'cascade' }),
|
||||||
|
|
||||||
kind: text('kind', { enum: ['image', 'file'] }).notNull().default('file'),
|
kind: text('kind', { enum: ['image', 'file'] })
|
||||||
|
.notNull()
|
||||||
|
.default('file'),
|
||||||
position: integer('position').notNull().default(0),
|
position: integer('position').notNull().default(0),
|
||||||
|
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
size: integer('size').notNull(),
|
size: integer('size').notNull(),
|
||||||
mimeType: text('mime_type').notNull(),
|
mimeType: text('mime_type').notNull(),
|
||||||
|
|
||||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date())
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ export const imapAuth = () => ({
|
|||||||
const adapter = ctx.context.internalAdapter;
|
const adapter = ctx.context.internalAdapter;
|
||||||
const existingUser = await adapter.findUserByEmail(email, { includeAccounts: true });
|
const existingUser = await adapter.findUserByEmail(email, { includeAccounts: true });
|
||||||
|
|
||||||
const credentialAccount = existingUser?.accounts.find((account) => account.providerId === 'credential');
|
const credentialAccount = existingUser?.accounts.find(
|
||||||
|
(account) => account.providerId === 'credential'
|
||||||
|
);
|
||||||
const credentialPassword = credentialAccount?.password;
|
const credentialPassword = credentialAccount?.password;
|
||||||
const isDbPasswordValid =
|
const isDbPasswordValid =
|
||||||
typeof credentialPassword === 'string' &&
|
typeof credentialPassword === 'string' &&
|
||||||
|
|||||||
@@ -9,3 +9,9 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,183 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import Navbar from '$lib/components/ui/Navbar.svelte';
|
||||||
|
import BoardCanvas from '$lib/components/board/BoardCanvas.svelte';
|
||||||
|
import Minimap from '$lib/components/ui/Minimap.svelte';
|
||||||
|
import type { Note } from '$lib/editor';
|
||||||
|
import { BOARD_HEIGHT, BOARD_WIDTH } from '$lib/board/constants';
|
||||||
|
import type { BoardNote, DragState, PanState, ViewportState } from '$lib/board/types';
|
||||||
|
import { clamp, clampNoteX, clampNoteY, getRandomPosition } from '$lib/board/utils';
|
||||||
|
|
||||||
|
let notes = $state<BoardNote[]>([]);
|
||||||
|
let drag = $state<DragState>(null);
|
||||||
|
let pan = $state<PanState>(null);
|
||||||
|
let maxZ = $state(1);
|
||||||
|
|
||||||
|
let viewport = $state<ViewportState>({
|
||||||
|
scrollLeft: 0,
|
||||||
|
scrollTop: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
let viewportEl: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
const updateViewportMetrics = () => {
|
||||||
|
if (!viewportEl) return;
|
||||||
|
|
||||||
|
viewport = {
|
||||||
|
scrollLeft: viewportEl.scrollLeft,
|
||||||
|
scrollTop: viewportEl.scrollTop,
|
||||||
|
width: viewportEl.clientWidth,
|
||||||
|
height: viewportEl.clientHeight
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateViewportMetrics();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreate = (note: Note) => {
|
||||||
|
const pos = getRandomPosition(notes.length);
|
||||||
|
maxZ += 1;
|
||||||
|
notes = [...notes, { ...note, x: pos.x, y: pos.y, z: maxZ }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const bringToFront = (id: string) => {
|
||||||
|
maxZ += 1;
|
||||||
|
notes = notes.map((note) => (note.id === id ? { ...note, z: maxZ } : note));
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDrag = (event: PointerEvent, id: string) => {
|
||||||
|
const element = event.currentTarget as HTMLElement;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
drag = {
|
||||||
|
id,
|
||||||
|
offsetX: event.clientX - rect.left,
|
||||||
|
offsetY: event.clientY - rect.top
|
||||||
|
};
|
||||||
|
bringToFront(id);
|
||||||
|
element.setPointerCapture(event.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
drag = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveDrag = (event: PointerEvent) => {
|
||||||
|
if (!drag || !viewportEl) return;
|
||||||
|
|
||||||
|
const viewportRect = viewportEl.getBoundingClientRect();
|
||||||
|
const pointerBoardX = viewportEl.scrollLeft + (event.clientX - viewportRect.left);
|
||||||
|
const pointerBoardY = viewportEl.scrollTop + (event.clientY - viewportRect.top);
|
||||||
|
|
||||||
|
const nextX = clampNoteX(pointerBoardX - drag.offsetX);
|
||||||
|
const nextY = clampNoteY(pointerBoardY - drag.offsetY);
|
||||||
|
|
||||||
|
notes = notes.map((note) => (note.id === drag?.id ? { ...note, x: nextX, y: nextY } : note));
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPan = (event: PointerEvent) => {
|
||||||
|
if (!viewportEl || drag) return;
|
||||||
|
|
||||||
|
pan = {
|
||||||
|
startX: event.clientX,
|
||||||
|
startY: event.clientY,
|
||||||
|
scrollLeft: viewportEl.scrollLeft,
|
||||||
|
scrollTop: viewportEl.scrollTop
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const movePan = (event: PointerEvent) => {
|
||||||
|
if (!pan || !viewportEl || drag) return;
|
||||||
|
|
||||||
|
const dx = event.clientX - pan.startX;
|
||||||
|
const dy = event.clientY - pan.startY;
|
||||||
|
|
||||||
|
viewportEl.scrollLeft = pan.scrollLeft - dx;
|
||||||
|
viewportEl.scrollTop = pan.scrollTop - dy;
|
||||||
|
updateViewportMetrics();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPan = () => {
|
||||||
|
pan = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateTo = (scrollLeft: number, scrollTop: number) => {
|
||||||
|
if (!viewportEl) return;
|
||||||
|
|
||||||
|
viewportEl.scrollLeft = clamp(scrollLeft, 0, BOARD_WIDTH - viewportEl.clientWidth);
|
||||||
|
viewportEl.scrollTop = clamp(scrollTop, 0, BOARD_HEIGHT - viewportEl.clientHeight);
|
||||||
|
updateViewportMetrics();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<svelte:window
|
||||||
|
onresize={updateViewportMetrics}
|
||||||
|
onpointermove={(event) => {
|
||||||
|
moveDrag(event);
|
||||||
|
movePan(event);
|
||||||
|
}}
|
||||||
|
onpointerup={() => {
|
||||||
|
stopDrag();
|
||||||
|
stopPan();
|
||||||
|
}}
|
||||||
|
onpointercancel={() => {
|
||||||
|
stopDrag();
|
||||||
|
stopPan();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="board-viewport" bind:this={viewportEl} onscroll={updateViewportMetrics}>
|
||||||
|
<div class="top-composer-shell">
|
||||||
|
<div class="top-composer">
|
||||||
|
<Navbar onCreate={handleCreate} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BoardCanvas notes={notes} onStartPan={startPan} onStartDrag={startDrag} />
|
||||||
|
|
||||||
|
<Minimap notes={notes} {viewport} onNavigate={navigateTo} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-viewport {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, #f8fbff, #eef2f7 45%, #e6ebf2 100%);
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-viewport:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-viewport::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-composer-shell {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 80;
|
||||||
|
padding: 16px 14px 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-composer {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,7 +6,8 @@ const config = {
|
|||||||
adapter: adapter()
|
adapter: adapter()
|
||||||
},
|
},
|
||||||
vitePlugin: {
|
vitePlugin: {
|
||||||
dynamicCompileOptions: ({ filename }) => filename.includes('node_modules') ? undefined : { runes: true }
|
dynamicCompileOptions: ({ filename }) =>
|
||||||
|
filename.includes('node_modules') ? undefined : { runes: true }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user