Add BMAD framework, authentication, and new features

This commit is contained in:
2026-01-08 21:23:23 +01:00
parent f07d28aefd
commit 15a95fb319
1298 changed files with 73308 additions and 154901 deletions

16
keep-notes/.dockerignore Normal file
View File

@@ -0,0 +1,16 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
build
dist
playwright-report
test-results

60
keep-notes/Dockerfile Normal file
View File

@@ -0,0 +1,60 @@
FROM node:22-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Build Next.js
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy Prisma schema and migrations for runtime usage if needed
COPY --from=builder /app/prisma ./prisma
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

View File

@@ -0,0 +1,29 @@
'use server';
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
console.error('AuthError details:', error.type, error.message);
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return `Auth error: ${error.type}`;
}
}
// IMPORTANT: Next.js redirects throw a special error that must be rethrown
if (error instanceof Error && error.message === 'NEXT_REDIRECT') {
throw error;
}
console.error('Non-AuthError during signin:', error);
throw error;
}
}

View File

@@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'
import prisma from '@/lib/prisma'
import { Note, CheckItem } from '@/lib/types'
import { auth } from '@/auth'
// Helper function to parse JSON strings from database
function parseNote(dbNote: any): Note {
@@ -11,14 +12,21 @@ function parseNote(dbNote: any): Note {
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
links: dbNote.links ? JSON.parse(dbNote.links) : null,
}
}
// Get all notes (non-archived by default)
export async function getNotes(includeArchived = false) {
const session = await auth();
if (!session?.user?.id) return [];
try {
const notes = await prisma.note.findMany({
where: includeArchived ? {} : { isArchived: false },
where: {
userId: session.user.id,
isArchived: includeArchived ? {} : { isArchived: false },
},
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
@@ -35,9 +43,15 @@ export async function getNotes(includeArchived = false) {
// Get archived notes only
export async function getArchivedNotes() {
const session = await auth();
if (!session?.user?.id) return [];
try {
const notes = await prisma.note.findMany({
where: { isArchived: true },
where: {
userId: session.user.id,
isArchived: true
},
orderBy: { updatedAt: 'desc' }
})
@@ -50,6 +64,9 @@ export async function getArchivedNotes() {
// Search notes
export async function searchNotes(query: string) {
const session = await auth();
if (!session?.user?.id) return [];
try {
if (!query.trim()) {
return await getNotes()
@@ -57,6 +74,7 @@ export async function searchNotes(query: string) {
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
isArchived: false,
OR: [
{ title: { contains: query } },
@@ -72,7 +90,7 @@ export async function searchNotes(query: string) {
})
// Enhanced ranking: prioritize title matches
const rankedNotes = notes.map(note => {
const rankedNotes = notes.map((note: any) => {
const parsedNote = parseNote(note)
let score = 0
@@ -101,8 +119,8 @@ export async function searchNotes(query: string) {
// Sort by score descending, then by existing order (pinned/updated)
return rankedNotes
.sort((a, b) => b.score - a.score)
.map(item => item.note)
.sort((a: any, b: any) => b.score - a.score)
.map((item: any) => item.note)
} catch (error) {
console.error('Error searching notes:', error)
return []
@@ -118,13 +136,18 @@ export async function createNote(data: {
checkItems?: CheckItem[]
labels?: string[]
images?: string[]
links?: any[]
isArchived?: boolean
reminder?: Date | null
isMarkdown?: boolean
}) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
const note = await prisma.note.create({
data: {
userId: session.user.id,
title: data.title || null,
content: data.content,
color: data.color || 'default',
@@ -132,6 +155,7 @@ export async function createNote(data: {
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
labels: data.labels ? JSON.stringify(data.labels) : null,
images: data.images ? JSON.stringify(data.images) : null,
links: data.links ? JSON.stringify(data.links) : null,
isArchived: data.isArchived || false,
reminder: data.reminder || null,
isMarkdown: data.isMarkdown || false,
@@ -157,9 +181,13 @@ export async function updateNote(id: string, data: {
checkItems?: CheckItem[] | null
labels?: string[] | null
images?: string[] | null
links?: any[] | null
reminder?: Date | null
isMarkdown?: boolean
}) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
// Stringify JSON fields if they exist
const updateData: any = { ...data }
@@ -172,10 +200,16 @@ export async function updateNote(id: string, data: {
if ('images' in data) {
updateData.images = data.images ? JSON.stringify(data.images) : null
}
if ('links' in data) {
updateData.links = data.links ? JSON.stringify(data.links) : null
}
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id },
where: {
id,
userId: session.user.id
},
data: updateData
})
@@ -189,9 +223,15 @@ export async function updateNote(id: string, data: {
// Delete a note
export async function deleteNote(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
await prisma.note.delete({
where: { id }
where: {
id,
userId: session.user.id
}
})
revalidatePath('/')
@@ -230,7 +270,7 @@ export async function getAllLabels() {
})
const labelsSet = new Set<string>()
notes.forEach(note => {
notes.forEach((note: any) => {
const labels = note.labels ? JSON.parse(note.labels) : null
if (labels) {
labels.forEach((label: string) => labelsSet.add(label))
@@ -246,11 +286,14 @@ export async function getAllLabels() {
// Reorder notes (drag and drop)
export async function reorderNotes(draggedId: string, targetId: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
console.log('[REORDER-DEBUG] reorderNotes called:', { draggedId, targetId })
try {
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId, userId: session.user.id } })
const targetNote = await prisma.note.findUnique({ where: { id: targetId, userId: session.user.id } })
console.log('[REORDER-DEBUG] Notes found:', {
draggedNote: draggedNote ? { id: draggedNote.id, title: draggedNote.title, isPinned: draggedNote.isPinned, order: draggedNote.order } : null,
@@ -265,23 +308,24 @@ export async function reorderNotes(draggedId: string, targetId: string) {
// Get all notes in the same category (pinned or unpinned)
const allNotes = await prisma.note.findMany({
where: {
userId: session.user.id,
isPinned: draggedNote.isPinned,
isArchived: false
},
orderBy: { order: 'asc' }
})
console.log('[REORDER-DEBUG] All notes in category:', allNotes.map(n => ({ id: n.id, title: n.title, order: n.order })))
console.log('[REORDER-DEBUG] All notes in category:', allNotes.map((n: any) => ({ id: n.id, title: n.title, order: n.order })))
// Create new order array
const reorderedNotes = allNotes.filter(n => n.id !== draggedId)
const targetIndex = reorderedNotes.findIndex(n => n.id === targetId)
const reorderedNotes = allNotes.filter((n: any) => n.id !== draggedId)
const targetIndex = reorderedNotes.findIndex((n: any) => n.id === targetId)
reorderedNotes.splice(targetIndex, 0, draggedNote)
console.log('[REORDER-DEBUG] New order:', reorderedNotes.map((n, i) => ({ id: n.id, title: n.title, newOrder: i })))
console.log('[REORDER-DEBUG] New order:', reorderedNotes.map((n: any, i: number) => ({ id: n.id, title: n.title, newOrder: i })))
// Update all notes with new order
const updates = reorderedNotes.map((note, index) =>
const updates = reorderedNotes.map((note: any, index: number) =>
prisma.note.update({
where: { id: note.id },
data: { order: index }
@@ -299,3 +343,27 @@ export async function reorderNotes(draggedId: string, targetId: string) {
throw new Error('Failed to reorder notes')
}
}
// Update full order of notes
export async function updateFullOrder(ids: string[]) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
const userId = session.user.id;
try {
const updates = ids.map((id: string, index: number) =>
prisma.note.update({
where: { id, userId },
data: { order: index }
})
)
await prisma.$transaction(updates)
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error updating full order:', error)
throw new Error('Failed to update order')
}
}

View File

@@ -0,0 +1,54 @@
'use server';
import bcrypt from 'bcryptjs';
import prisma from '@/lib/prisma';
import { z } from 'zod';
import { redirect } from 'next/navigation';
const RegisterSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
name: z.string().min(2),
});
export async function register(prevState: string | undefined, formData: FormData) {
const validatedFields = RegisterSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
name: formData.get('name'),
});
if (!validatedFields.success) {
return 'Invalid fields. Failed to register.';
}
const { email, password, name } = validatedFields.data;
try {
const existingUser = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
if (existingUser) {
return 'User already exists.';
}
const hashedPassword = await bcrypt.hash(password, 10);
await prisma.user.create({
data: {
email: email.toLowerCase(),
password: hashedPassword,
name,
},
});
// Attempt to sign in immediately after registration
// We cannot import signIn here directly if it causes circular deps or issues,
// but usually it works. If not, redirecting to login is fine.
// Let's stick to redirecting to login but with a clear success message?
// Or better: lowercase the email to fix the potential bug.
} catch (error) {
console.error('Registration Error:', error);
return 'Database Error: Failed to create user.';
}
redirect('/login');
}

View File

@@ -0,0 +1,54 @@
'use server'
import * as cheerio from 'cheerio';
export interface LinkMetadata {
url: string;
title?: string;
description?: string;
imageUrl?: string;
siteName?: string;
}
export async function fetchLinkMetadata(url: string): Promise<LinkMetadata | null> {
try {
// Add protocol if missing
let targetUrl = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
targetUrl = 'https://' + url;
}
const response = await fetch(targetUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; Memento/1.0; +http://localhost:3000)',
},
next: { revalidate: 3600 } // Cache for 1 hour
});
if (!response.ok) return null;
const html = await response.text();
const $ = cheerio.load(html);
const getMeta = (prop: string) =>
$(`meta[property="${prop}"]`).attr('content') ||
$(`meta[name="${prop}"]`).attr('content');
const title = getMeta('og:title') || $('title').text() || '';
const description = getMeta('og:description') || getMeta('description') || '';
const imageUrl = getMeta('og:image');
const siteName = getMeta('og:site_name');
return {
url: targetUrl,
title: title.substring(0, 100), // Truncate if too long
description: description.substring(0, 200),
imageUrl,
siteName
};
} catch (error) {
console.error('Error fetching link metadata:', error);
return null;
}
}

View File

@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import { LABEL_COLORS } from '@/lib/types';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const labels = await prisma.label.findMany();
const colors = Object.keys(LABEL_COLORS).filter(c => c !== 'gray'); // Exclude gray to force colors
const updates = labels.map((label: any) => {
const randomColor = colors[Math.floor(Math.random() * colors.length)];
return prisma.label.update({
where: { id: label.id },
data: { color: randomColor }
});
});
await prisma.$transaction(updates);
return NextResponse.json({
success: true,
updated: updates.length,
message: "All labels have been assigned a random non-gray color."
});
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View File

@@ -0,0 +1,57 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
// 1. Get all notes
const notes = await prisma.note.findMany({
select: { labels: true }
});
// 2. Extract all unique labels from JSON
const uniqueLabels = new Set<string>();
notes.forEach((note: any) => {
if (note.labels) {
try {
const parsed = JSON.parse(note.labels);
if (Array.isArray(parsed)) {
parsed.forEach((l: string) => uniqueLabels.add(l));
}
} catch (e) {
// ignore error
}
}
});
// 3. Get existing labels in DB
const existingDbLabels = await prisma.label.findMany();
const existingNames = new Set(existingDbLabels.map((l: any) => l.name));
// 4. Create missing labels
const created = [];
for (const name of uniqueLabels) {
if (!existingNames.has(name)) {
const newLabel = await prisma.label.create({
data: {
name,
color: 'gray' // Default color
}
});
created.push(newLabel);
}
}
return NextResponse.json({
success: true,
foundInNotes: uniqueLabels.size,
alreadyInDb: existingNames.size,
created: created.length,
createdLabels: created
});
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View File

@@ -0,0 +1,2 @@
import { handlers } from "@/auth"
export const { GET, POST } = handlers

View File

@@ -0,0 +1,62 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export const dynamic = 'force-dynamic'; // No caching
export async function POST(request: Request) {
try {
const now = new Date();
// 1. Find all due reminders that haven't been processed
const dueNotes = await prisma.note.findMany({
where: {
reminder: {
lte: now, // Less than or equal to now
},
isReminderDone: false,
isArchived: false, // Optional: exclude archived notes
},
select: {
id: true,
title: true,
content: true,
reminder: true,
// Add other fields useful for notification
},
});
if (dueNotes.length === 0) {
return NextResponse.json({
success: true,
count: 0,
message: 'No due reminders found'
});
}
// 2. Mark them as done (Atomic operation logic would be better but simple batch update is fine here)
const noteIds = dueNotes.map((n: any) => n.id);
await prisma.note.updateMany({
where: {
id: { in: noteIds }
},
data: {
isReminderDone: true
}
});
// 3. Return the notes to N8N so it can send emails/messages
return NextResponse.json({
success: true,
count: dueNotes.length,
reminders: dueNotes
});
} catch (error) {
console.error('Error processing cron reminders:', error);
return NextResponse.json(
{ success: false, error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -1,10 +1,19 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
const COLORS = ['red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray'];
// GET /api/labels - Get all labels
export async function GET(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const labels = await prisma.label.findMany({
where: { userId: session.user.id },
orderBy: { name: 'asc' }
})
@@ -23,6 +32,11 @@ export async function GET(request: NextRequest) {
// POST /api/labels - Create a new label
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json()
const { name, color } = body
@@ -34,9 +48,14 @@ export async function POST(request: NextRequest) {
)
}
// Check if label already exists
// Check if label already exists for this user
const existing = await prisma.label.findUnique({
where: { name: name.trim() }
where: {
name_userId: {
name: name.trim(),
userId: session.user.id
}
}
})
if (existing) {
@@ -49,7 +68,8 @@ export async function POST(request: NextRequest) {
const label = await prisma.label.create({
data: {
name: name.trim(),
color: color || 'gray'
color: color || COLORS[Math.floor(Math.random() * COLORS.length)],
userId: session.user.id
}
})

View File

@@ -13,11 +13,12 @@ function parseNote(dbNote: any) {
// GET /api/notes/[id] - Get a single note
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const note = await prisma.note.findUnique({
where: { id: params.id }
where: { id }
})
if (!note) {
@@ -43,9 +44,10 @@ export async function GET(
// PUT /api/notes/[id] - Update a note
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const updateData: any = { ...body }
@@ -59,7 +61,7 @@ export async function PUT(
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id: params.id },
where: { id },
data: updateData
})
@@ -79,11 +81,12 @@ export async function PUT(
// DELETE /api/notes/[id] - Delete a note
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
await prisma.note.delete({
where: { id: params.id }
where: { id }
})
return NextResponse.json({

View File

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'
import { writeFile, mkdir } from 'fs/promises'
import path from 'path'
import { randomUUID } from 'crypto'
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ error: 'No file uploaded' },
{ status: 400 }
)
}
const buffer = Buffer.from(await file.arrayBuffer())
const filename = `${randomUUID()}${path.extname(file.name)}`
// Ensure directory exists
const uploadDir = path.join(process.cwd(), 'public/uploads/notes')
await mkdir(uploadDir, { recursive: true })
const filePath = path.join(uploadDir, filename)
await writeFile(filePath, buffer)
return NextResponse.json({
success: true,
url: `/uploads/notes/${filename}`
})
} catch (error) {
console.error('Upload error:', error)
return NextResponse.json(
{ error: 'Failed to upload file' },
{ status: 500 }
)
}
}

View File

@@ -1,5 +1,5 @@
import { getArchivedNotes } from '@/app/actions/notes'
import { NoteGrid } from '@/components/note-grid'
import { MasonryGrid } from '@/components/masonry-grid'
export const dynamic = 'force-dynamic'
@@ -9,7 +9,7 @@ export default async function ArchivePage() {
return (
<main className="container mx-auto px-4 py-8 max-w-7xl">
<h1 className="text-3xl font-bold mb-8">Archive</h1>
<NoteGrid notes={notes} />
<MasonryGrid notes={notes} />
</main>
)
}

View File

@@ -115,6 +115,48 @@
--sidebar-ring: oklch(0.556 0 0);
}
[data-theme='midnight'] {
--background: oklch(0.18 0.04 260);
--foreground: oklch(0.985 0 0);
--card: oklch(0.22 0.05 260);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.22 0.05 260);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.7 0.15 260);
--primary-foreground: oklch(0.18 0.04 260);
--secondary: oklch(0.28 0.05 260);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.28 0.05 260);
--muted-foreground: oklch(0.8 0.05 260);
--accent: oklch(0.28 0.05 260);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.6 0.25 25);
--border: oklch(0.3 0.05 260);
--input: oklch(0.3 0.05 260);
--ring: oklch(0.7 0.15 260);
}
[data-theme='sepia'] {
--background: oklch(0.96 0.02 85);
--foreground: oklch(0.25 0.02 85);
--card: oklch(0.98 0.01 85);
--card-foreground: oklch(0.25 0.02 85);
--popover: oklch(0.98 0.01 85);
--popover-foreground: oklch(0.25 0.02 85);
--primary: oklch(0.45 0.1 35);
--primary-foreground: oklch(0.98 0.01 85);
--secondary: oklch(0.92 0.03 85);
--secondary-foreground: oklch(0.25 0.02 85);
--muted: oklch(0.92 0.03 85);
--muted-foreground: oklch(0.5 0.05 85);
--accent: oklch(0.92 0.03 85);
--accent-foreground: oklch(0.25 0.02 85);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.85 0.05 85);
--input: oklch(0.85 0.05 85);
--ring: oklch(0.45 0.1 35);
}
@layer base {
* {
@apply border-border outline-ring/50;

View File

@@ -4,6 +4,7 @@ import "./globals.css";
import { HeaderWrapper } from "@/components/header-wrapper";
import { ToastProvider } from "@/components/ui/toast";
import { LabelProvider } from "@/context/LabelContext";
import { Sidebar } from "@/components/sidebar";
const inter = Inter({
subsets: ["latin"],
@@ -24,8 +25,15 @@ export default function RootLayout({
<body className={inter.className}>
<ToastProvider>
<LabelProvider>
<HeaderWrapper />
{children}
<div className="flex min-h-screen flex-col">
<HeaderWrapper />
<div className="flex flex-1">
<Sidebar className="shrink-0 border-r" />
<main className="flex-1">
{children}
</main>
</div>
</div>
</LabelProvider>
</ToastProvider>
</body>

View File

@@ -0,0 +1,11 @@
import { LoginForm } from '@/components/login-form';
export default function LoginPage() {
return (
<main className="flex items-center justify-center md:h-screen">
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
<LoginForm />
</div>
</main>
);
}

View File

@@ -5,14 +5,18 @@ import { useSearchParams } from 'next/navigation'
import { Note } from '@/lib/types'
import { getNotes, searchNotes } from '@/app/actions/notes'
import { NoteInput } from '@/components/note-input'
import { NoteGrid } from '@/components/note-grid'
import { MasonryGrid } from '@/components/masonry-grid'
import { useLabels } from '@/context/LabelContext'
import { useReminderCheck } from '@/hooks/use-reminder-check'
export default function HomePage() {
const searchParams = useSearchParams()
const [notes, setNotes] = useState<Note[]>([])
const [isLoading, setIsLoading] = useState(true)
const { labels } = useLabels()
// Enable reminder notifications
useReminderCheck(notes)
useEffect(() => {
const loadNotes = async () => {
@@ -25,19 +29,19 @@ export default function HomePage() {
// Filter by selected labels
if (labelFilter.length > 0) {
allNotes = allNotes.filter(note =>
note.labels?.some(label => labelFilter.includes(label))
allNotes = allNotes.filter((note: any) =>
note.labels?.some((label: string) => labelFilter.includes(label))
)
}
// Filter by color (filter notes that have labels with this color)
if (colorFilter) {
const labelNamesWithColor = labels
.filter(label => label.color === colorFilter)
.map(label => label.name)
.filter((label: any) => label.color === colorFilter)
.map((label: any) => label.name)
allNotes = allNotes.filter(note =>
note.labels?.some(label => labelNamesWithColor.includes(label))
allNotes = allNotes.filter((note: any) =>
note.labels?.some((label: string) => labelNamesWithColor.includes(label))
)
}
@@ -54,7 +58,7 @@ export default function HomePage() {
{isLoading ? (
<div className="text-center py-8 text-gray-500">Loading...</div>
) : (
<NoteGrid notes={notes} />
<MasonryGrid notes={notes} />
)}
</main>
)

View File

@@ -0,0 +1,11 @@
import { RegisterForm } from '@/components/register-form';
export default function RegisterPage() {
return (
<main className="flex items-center justify-center md:h-screen">
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
<RegisterForm />
</div>
</main>
);
}

43
keep-notes/auth.config.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
newUser: '/register',
},
secret: "csQFtfYvQ8YtatEYSUFyslXdk2vJhZFt9D5gav/RJQg=",
trustHost: true,
session: {
strategy: 'jwt',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isDashboardPage = nextUrl.pathname === '/' ||
nextUrl.pathname.startsWith('/reminders') ||
nextUrl.pathname.startsWith('/archive') ||
nextUrl.pathname.startsWith('/trash');
if (isDashboardPage) {
if (isLoggedIn) return true;
return false;
} else if (isLoggedIn && (nextUrl.pathname === '/login' || nextUrl.pathname === '/register')) {
return Response.redirect(new URL('/', nextUrl));
}
return true;
},
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (token && session.user) {
(session.user as any).id = token.id;
}
return session;
},
},
providers: [],
} satisfies NextAuthConfig;

52
keep-notes/auth.ts Normal file
View File

@@ -0,0 +1,52 @@
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
import prisma from '@/lib/prisma';
import bcrypt from 'bcryptjs';
export const { auth, signIn, signOut, handlers } = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
try {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);
if (!parsedCredentials.success) {
console.error('Invalid credentials format');
return null;
}
const { email, password } = parsedCredentials.data;
const user = await prisma.user.findUnique({
where: { email: email.toLowerCase() }
});
if (!user || !user.password) {
console.error('User not found or no password set');
return null;
}
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) {
return {
id: user.id,
email: user.email,
name: user.name,
};
}
console.error('Password mismatch');
return null;
} catch (error) {
console.error('CRITICAL AUTH ERROR:', error);
return null;
}
},
}),
],
});

View File

@@ -0,0 +1,33 @@
import { Button } from "@/components/ui/button"
import { X } from "lucide-react"
interface EditorImagesProps {
images: string[]
onRemove: (index: number) => void
}
export function EditorImages({ images, onRemove }: EditorImagesProps) {
if (!images || images.length === 0) return null
return (
<div className="flex flex-col gap-3 mb-4">
{images.map((img, idx) => (
<div key={idx} className="relative group">
<img
src={img}
alt=""
className="h-auto rounded-lg"
/>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-7 w-7 p-0 bg-white/90 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => onRemove(idx)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)
}

View File

@@ -3,18 +3,26 @@
import { useState, useEffect } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Menu, Search, Archive, StickyNote, Tag, Moon, Sun } from 'lucide-react'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import { Menu, Search, StickyNote, Tag, Moon, Sun, X, Bell, Trash2, Archive } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/lib/utils'
import { searchNotes } from '@/app/actions/notes'
import { useRouter } from 'next/navigation'
import { useLabels } from '@/context/LabelContext'
import { LabelManagementDialog } from './label-management-dialog'
import { LabelFilter } from './label-filter'
interface HeaderProps {
@@ -24,129 +32,279 @@ interface HeaderProps {
onColorFilterChange?: (color: string | null) => void
}
export function Header({ selectedLabels = [], selectedColor, onLabelFilterChange, onColorFilterChange }: HeaderProps = {}) {
export function Header({
selectedLabels = [],
selectedColor = null,
onLabelFilterChange,
onColorFilterChange
}: HeaderProps = {}) {
const [searchQuery, setSearchQuery] = useState('')
const [isSearching, setIsSearching] = useState(false)
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const pathname = usePathname()
const router = useRouter()
const searchParams = useSearchParams()
const { labels } = useLabels()
const currentLabels = searchParams.get('labels')?.split(',').filter(Boolean) || []
const currentSearch = searchParams.get('search') || ''
const currentColor = searchParams.get('color') || ''
useEffect(() => {
// Check for saved theme or system preference
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const initialTheme = savedTheme || systemTheme
setSearchQuery(currentSearch)
}, [currentSearch])
setTheme(initialTheme)
document.documentElement.classList.toggle('dark', initialTheme === 'dark')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light'
applyTheme(savedTheme)
}, [])
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
const applyTheme = (newTheme: string) => {
setTheme(newTheme as any)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}
// Remove all theme classes first
document.documentElement.classList.remove('dark')
document.documentElement.removeAttribute('data-theme')
const handleSearch = async (query: string) => {
setSearchQuery(query)
if (query.trim()) {
setIsSearching(true)
// Search functionality will be handled by the parent component
// For now, we'll just update the URL
router.push(`/?search=${encodeURIComponent(query)}`)
setIsSearching(false)
} else {
router.push('/')
if (newTheme === 'dark') {
document.documentElement.classList.add('dark')
} else if (newTheme !== 'light') {
document.documentElement.setAttribute('data-theme', newTheme)
if (newTheme === 'midnight') {
document.documentElement.classList.add('dark')
}
}
}
const handleSearch = (query: string) => {
setSearchQuery(query)
const params = new URLSearchParams(searchParams.toString())
if (query.trim()) {
params.set('search', query)
} else {
params.delete('search')
}
router.push(`/?${params.toString()}`)
}
const removeLabelFilter = (labelToRemove: string) => {
const newLabels = currentLabels.filter(l => l !== labelToRemove)
const params = new URLSearchParams(searchParams.toString())
if (newLabels.length > 0) {
params.set('labels', newLabels.join(','))
} else {
params.delete('labels')
}
router.push(`/?${params.toString()}`)
}
const removeColorFilter = () => {
const params = new URLSearchParams(searchParams.toString())
params.delete('color')
router.push(`/?${params.toString()}`)
}
const clearAllFilters = () => {
setSearchQuery('')
router.push('/')
}
const handleFilterChange = (newLabels: string[]) => {
const params = new URLSearchParams(searchParams.toString())
if (newLabels.length > 0) {
params.set('labels', newLabels.join(','))
} else {
params.delete('labels')
}
router.push(`/?${params.toString()}`)
}
const handleColorChange = (newColor: string | null) => {
const params = new URLSearchParams(searchParams.toString())
if (newColor) {
params.set('color', newColor)
} else {
params.delete('color')
}
router.push(`/?${params.toString()}`)
}
const NavItem = ({ href, icon: Icon, label, active }: any) => (
<Link
href={href}
onClick={() => setIsSidebarOpen(false)}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2",
active
? "bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
>
<Icon className={cn("h-5 w-5", active && "fill-current")} />
{label}
</Link>
)
const hasActiveFilters = currentLabels.length > 0 || !!currentSearch || !!currentColor
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center px-4 gap-4">
{/* Mobile Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem asChild>
<Link href="/" className="flex items-center">
<StickyNote className="h-4 w-4 mr-2" />
Notes
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/archive" className="flex items-center">
<Archive className="h-4 w-4 mr-2" />
Archive
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<>
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex flex-col transition-all duration-200">
<div className="flex h-16 items-center px-4 gap-4 shrink-0">
<Sheet open={isSidebarOpen} onOpenChange={setIsSidebarOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="-ml-2 md:hidden">
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[280px] sm:w-[320px] p-0 pt-4">
<SheetHeader className="px-4 mb-4">
<SheetTitle className="flex items-center gap-2 text-xl font-normal text-amber-500">
<StickyNote className="h-6 w-6" />
Memento
</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-1 py-2">
<NavItem
href="/"
icon={StickyNote}
label="Notes"
active={pathname === '/' && !hasActiveFilters}
/>
<NavItem
href="/reminders"
icon={Bell}
label="Reminders"
active={pathname === '/reminders'}
/>
<div className="my-2 px-4 flex items-center justify-between">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</span>
<LabelManagementDialog />
</div>
{labels.map(label => (
<NavItem
key={label.id}
href={`/?labels=${encodeURIComponent(label.name)}`}
icon={Tag}
label={label.name}
active={currentLabels.includes(label.name)}
/>
))}
{/* Logo */}
<Link href="/" className="flex items-center gap-2">
<StickyNote className="h-6 w-6 text-yellow-500" />
<span className="font-semibold text-xl hidden sm:inline-block">Memento</span>
</Link>
<div className="my-2 border-t border-gray-200 dark:border-zinc-800" />
<NavItem
href="/archive"
icon={Archive}
label="Archive"
active={pathname === '/archive'}
/>
<NavItem
href="/trash"
icon={Trash2}
label="Trash"
active={pathname === '/trash'}
/>
</div>
</SheetContent>
</Sheet>
{/* Search Bar */}
<div className="flex-1 max-w-2xl flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search notes..."
className="pl-10"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
<Link href="/" className="flex items-center gap-2 mr-4">
<StickyNote className="h-7 w-7 text-amber-500" />
<span className="font-medium text-xl hidden sm:inline-block text-gray-600 dark:text-gray-200">
Memento
</span>
</Link>
<div className="flex-1 max-w-2xl relative">
<div className="relative group">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 group-focus-within:text-gray-600 dark:group-focus-within:text-gray-200 transition-colors" />
<Input
placeholder="Search"
className="pl-10 pr-12 h-11 bg-gray-100 dark:bg-zinc-800/50 border-transparent focus:bg-white dark:focus:bg-zinc-900 focus:border-gray-200 dark:focus:border-zinc-700 shadow-none transition-all"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
{searchQuery && (
<button
onClick={() => handleSearch('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<div className="absolute right-0 top-0 h-full flex items-center pr-2">
<LabelFilter
selectedLabels={currentLabels}
selectedColor={currentColor || null}
onFilterChange={handleFilterChange}
onColorChange={handleColorChange}
/>
</div>
</div>
<div className="flex items-center gap-1 sm:gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
{theme === 'light' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => applyTheme('light')}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => applyTheme('dark')}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => applyTheme('midnight')}>Midnight</DropdownMenuItem>
<DropdownMenuItem onClick={() => applyTheme('sepia')}>Sepia</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{onLabelFilterChange && (
<LabelFilter
selectedLabels={selectedLabels}
selectedColor={selectedColor}
onFilterChange={onLabelFilterChange}
onColorChange={onColorFilterChange}
/>
)}
</div>
{/* Theme Toggle */}
<Button variant="ghost" size="sm" onClick={toggleTheme}>
{theme === 'light' ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</Button>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex border-t">
<Link
href="/"
className={cn(
'flex items-center gap-2 px-6 py-3 text-sm font-medium transition-colors hover:bg-accent',
pathname === '/' && 'bg-accent'
)}
>
<StickyNote className="h-4 w-4" />
Notes
</Link>
<Link
href="/archive"
className={cn(
'flex items-center gap-2 px-6 py-3 text-sm font-medium transition-colors hover:bg-accent',
pathname === '/archive' && 'bg-accent'
)}
>
<Archive className="h-4 w-4" />
Archive
</Link>
</nav>
</header>
{hasActiveFilters && (
<div className="px-4 pb-3 flex items-center gap-2 overflow-x-auto border-t border-gray-100 dark:border-zinc-800 pt-2 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm animate-in slide-in-from-top-2">
{currentSearch && (
<Badge variant="secondary" className="flex items-center gap-1 h-7 whitespace-nowrap pl-2 pr-1">
Search: {currentSearch}
<button onClick={() => handleSearch('')} className="ml-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full p-0.5">
<X className="h-3 w-3" />
</button>
</Badge>
)}
{currentColor && (
<Badge variant="secondary" className="flex items-center gap-1 h-7 whitespace-nowrap pl-2 pr-1">
<div className={cn("w-3 h-3 rounded-full border border-black/10", `bg-${currentColor}-500`)} />
Color: {currentColor}
<button onClick={removeColorFilter} className="ml-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full p-0.5">
<X className="h-3 w-3" />
</button>
</Badge>
)}
{currentLabels.map(label => (
<Badge key={label} variant="secondary" className="flex items-center gap-1 h-7 whitespace-nowrap pl-2 pr-1">
<Tag className="h-3 w-3" />
{label}
<button onClick={() => removeLabelFilter(label)} className="ml-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full p-0.5">
<X className="h-3 w-3" />
</button>
</Badge>
))}
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="h-7 text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20 whitespace-nowrap ml-auto"
>
Clear all
</Button>
</div>
)}
</header>
</>
)
}

View File

@@ -0,0 +1,160 @@
'use client'
import { useState } from 'react'
import { Button } from './ui/button'
import { Input } from './ui/input'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from './ui/dialog'
import { Badge } from './ui/badge'
import { Settings, Plus, Palette, Trash2, Tag } from 'lucide-react'
import { LABEL_COLORS, LabelColorName } from '@/lib/types'
import { cn } from '@/lib/utils'
import { useLabels } from '@/context/LabelContext'
export function LabelManagementDialog() {
const { labels, loading, addLabel, updateLabel, deleteLabel } = useLabels()
const [newLabel, setNewLabel] = useState('')
const [editingColorId, setEditingColorId] = useState<string | null>(null)
const handleAddLabel = async () => {
const trimmed = newLabel.trim()
if (trimmed) {
try {
await addLabel(trimmed, 'gray')
setNewLabel('')
} catch (error) {
console.error('Failed to add label:', error)
}
}
}
const handleDeleteLabel = async (id: string) => {
if (confirm('Are you sure you want to delete this label?')) {
try {
await deleteLabel(id)
} catch (error) {
console.error('Failed to delete label:', error)
}
}
}
const handleChangeColor = async (id: string, color: LabelColorName) => {
try {
await updateLabel(id, { color })
setEditingColorId(null)
} catch (error) {
console.error('Failed to update label color:', error)
}
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" title="Manage Labels">
<Settings className="h-5 w-5" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Edit Labels</DialogTitle>
<DialogDescription>
Create, edit colors, or delete labels.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Add new label */}
<div className="flex gap-2">
<Input
placeholder="Create new label"
value={newLabel}
onChange={(e) => setNewLabel(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddLabel()
}
}}
/>
<Button onClick={handleAddLabel} size="icon">
<Plus className="h-4 w-4" />
</Button>
</div>
{/* List labels */}
<div className="max-h-[60vh] overflow-y-auto space-y-2">
{loading ? (
<p className="text-sm text-gray-500">Loading...</p>
) : labels.length === 0 ? (
<p className="text-sm text-gray-500">No labels found.</p>
) : (
labels.map((label) => {
const colorClasses = LABEL_COLORS[label.color]
const isEditing = editingColorId === label.id
return (
<div key={label.id} className="flex items-center justify-between p-2 rounded-md hover:bg-gray-100 dark:hover:bg-zinc-800/50 group">
<div className="flex items-center gap-3 flex-1 relative">
<Tag className={cn("h-4 w-4", colorClasses.text)} />
<span className="font-medium text-sm">{label.name}</span>
{/* Color Picker Popover */}
{isEditing && (
<div className="absolute z-20 top-8 left-0 bg-white dark:bg-zinc-900 border rounded-lg shadow-xl p-3 animate-in fade-in zoom-in-95 w-48">
<div className="grid grid-cols-5 gap-2">
{(Object.keys(LABEL_COLORS) as LabelColorName[]).map((color) => {
const classes = LABEL_COLORS[color]
return (
<button
key={color}
className={cn(
'h-7 w-7 rounded-full border-2 transition-all hover:scale-110',
classes.bg,
label.color === color ? 'border-gray-900 dark:border-gray-100 ring-2 ring-offset-1' : 'border-transparent'
)}
onClick={() => handleChangeColor(label.id, color)}
title={color}
/>
)
})}
</div>
</div>
)}
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
onClick={() => setEditingColorId(isEditing ? null : label.id)}
title="Change Color"
>
<Palette className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
onClick={() => handleDeleteLabel(label.id)}
title="Delete Label"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
)
})
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,9 +1,11 @@
'use client'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { useState } from 'react'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Tag } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Tag, Plus, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLabels } from '@/context/LabelContext'
import { LabelBadge } from './label-badge'
@@ -20,10 +22,15 @@ export function LabelSelector({
selectedLabels,
onLabelsChange,
variant = 'default',
triggerLabel = 'Tags',
triggerLabel = 'Labels',
align = 'start',
}: LabelSelectorProps) {
const { labels, loading } = useLabels()
const { labels, loading, addLabel } = useLabels()
const [search, setSearch] = useState('')
const filteredLabels = labels.filter(l =>
l.name.toLowerCase().includes(search.toLowerCase())
)
const handleToggleLabel = (labelName: string) => {
if (selectedLabels.includes(labelName)) {
@@ -33,39 +40,90 @@ export function LabelSelector({
}
}
const handleCreateLabel = async () => {
const trimmed = search.trim()
if (trimmed) {
await addLabel(trimmed) // Let backend assign random color
onLabelsChange([...selectedLabels, trimmed])
setSearch('')
}
}
const showCreateOption = search.trim() && !labels.some(l => l.name.toLowerCase() === search.trim().toLowerCase())
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8">
<Tag className="h-4 w-4 mr-2" />
<Button variant="ghost" size="sm" className="h-8 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 px-2">
<Tag className={cn("h-4 w-4", triggerLabel && "mr-2")} />
{triggerLabel}
{selectedLabels.length > 0 && (
<Badge variant="secondary" className="ml-2 h-5 min-w-5 px-1.5">
<Badge variant="secondary" className="ml-2 h-5 min-w-5 px-1.5 bg-gray-200 text-gray-800 dark:bg-zinc-700 dark:text-zinc-300">
{selectedLabels.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={align} className="w-64">
<div className="max-h-64 overflow-y-auto">
<DropdownMenuContent align={align} className="w-64 p-0">
<div className="p-2">
<Input
placeholder="Enter label name"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-8 text-sm"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
if (showCreateOption) handleCreateLabel()
}
}}
/>
</div>
<div className="max-h-64 overflow-y-auto px-1 pb-1">
{loading ? (
<div className="p-4 text-sm text-gray-500">Loading...</div>
) : labels.length === 0 ? (
<div className="p-4 text-sm text-gray-500">No labels yet</div>
<div className="p-2 text-sm text-gray-500 text-center">Loading...</div>
) : (
labels.map((label) => {
const isSelected = selectedLabels.includes(label.name)
<>
{filteredLabels.map((label) => {
const isSelected = selectedLabels.includes(label.name)
return (
<div
key={label.id}
onClick={(e) => {
e.preventDefault()
handleToggleLabel(label.name)
}}
className="flex items-center gap-2 p-2 rounded-sm hover:bg-gray-100 dark:hover:bg-zinc-800 cursor-pointer text-sm"
>
<div className={cn(
"h-4 w-4 border rounded flex items-center justify-center transition-colors",
isSelected ? "bg-blue-600 border-blue-600 text-white" : "border-gray-400"
)}>
{isSelected && <Check className="h-3 w-3" />}
</div>
<LabelBadge label={label.name} variant="clickable" />
</div>
)
})}
return (
<DropdownMenuItem
key={label.id}
onClick={() => handleToggleLabel(label.name)}
className="flex items-center justify-between gap-2"
{showCreateOption && (
<div
onClick={(e) => {
e.preventDefault()
handleCreateLabel()
}}
className="flex items-center gap-2 p-2 rounded-sm hover:bg-gray-100 dark:hover:bg-zinc-800 cursor-pointer text-sm border-t mt-1"
>
<LabelBadge label={label.name} isSelected={isSelected} />
</DropdownMenuItem>
)
})
<Plus className="h-4 w-4" />
<span>Create "{search}"</span>
</div>
)}
{filteredLabels.length === 0 && !showCreateOption && (
<div className="p-2 text-sm text-gray-500 text-center">No labels found</div>
)}
</>
)}
</div>
</DropdownMenuContent>

View File

@@ -0,0 +1,86 @@
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { authenticate } from '@/app/actions/auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import Link from 'next/link';
function LoginButton() {
const { pending } = useFormStatus();
return (
<Button className="w-full mt-4" aria-disabled={pending}>
Log in
</Button>
);
}
export function LoginForm() {
const [errorMessage, dispatch] = useActionState(authenticate, undefined);
return (
<form action={dispatch} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className="mb-3 text-2xl font-bold">
Please log in to continue.
</h1>
<div className="w-full">
<div>
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="email"
>
Email
</label>
<div className="relative">
<Input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="email"
type="email"
name="email"
placeholder="Enter your email address"
required
/>
</div>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="password"
>
Password
</label>
<div className="relative">
<Input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="password"
type="password"
name="password"
placeholder="Enter password"
required
minLength={6}
/>
</div>
</div>
</div>
<LoginButton />
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
<div className="mt-4 text-center text-sm">
Don't have an account?{' '}
<Link href="/register" className="underline">
Register
</Link>
</div>
</div>
</form>
);
}

View File

@@ -0,0 +1,203 @@
'use client';
import { Note } from '@/lib/types';
import { NoteCard } from './note-card';
import { useState, useEffect, useRef, useCallback } from 'react';
import { NoteEditor } from './note-editor';
import { updateFullOrder } from '@/app/actions/notes';
import { useResizeObserver } from '@/hooks/use-resize-observer';
interface MasonryGridProps {
notes: Note[];
}
interface MasonryItemProps {
note: Note;
onEdit: (note: Note) => void;
onResize: () => void;
}
function MasonryItem({ note, onEdit, onResize }: MasonryItemProps) {
const resizeRef = useResizeObserver(() => {
onResize();
});
return (
<div
className="masonry-item absolute w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5 p-2"
data-id={note.id}
ref={resizeRef as any}
>
<div className="masonry-item-content relative">
<NoteCard note={note} onEdit={onEdit} />
</div>
</div>
);
}
export function MasonryGrid({ notes }: MasonryGridProps) {
const [editingNote, setEditingNote] = useState<Note | null>(null);
const pinnedGridRef = useRef<HTMLDivElement>(null);
const othersGridRef = useRef<HTMLDivElement>(null);
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
const pinnedNotes = notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order);
const othersNotes = notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order);
const handleDragEnd = async (grid: any) => {
if (!grid) return;
const items = grid.getItems();
const ids = items
.map((item: any) => item.getElement()?.getAttribute('data-id'))
.filter((id: any): id is string => !!id);
try {
await updateFullOrder(ids);
} catch (error) {
console.error('Failed to persist order:', error);
}
};
const refreshLayout = useCallback(() => {
// Use requestAnimationFrame for smoother updates
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, []);
useEffect(() => {
let isMounted = true;
const initMuuri = async () => {
// Import web-animations-js polyfill
await import('web-animations-js');
// Dynamic import of Muuri to avoid SSR window error
const MuuriClass = (await import('muuri')).default;
if (!isMounted) return;
const layoutOptions = {
dragEnabled: true,
dragContainer: document.body,
dragStartPredicate: {
distance: 10,
delay: 0,
},
dragPlaceholder: {
enabled: true,
createElement: (item: any) => {
const el = item.getElement().cloneNode(true);
el.style.opacity = '0.5';
return el;
},
},
dragAutoScroll: {
targets: [window],
speed: (item: any, target: any, intersection: any) => {
return intersection * 20;
},
},
};
if (pinnedGridRef.current && !pinnedMuuri.current && pinnedNotes.length > 0) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
}
if (othersGridRef.current && !othersMuuri.current && othersNotes.length > 0) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
}
};
initMuuri();
return () => {
isMounted = false;
pinnedMuuri.current?.destroy();
othersMuuri.current?.destroy();
pinnedMuuri.current = null;
othersMuuri.current = null;
};
}, [pinnedNotes.length, othersNotes.length]);
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
}, [notes]);
return (
<div className="masonry-container">
{pinnedNotes.length > 0 && (
<div className="mb-8">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">Pinned</h2>
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={setEditingNote}
onResize={refreshLayout}
/>
))}
</div>
</div>
)}
{othersNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">Others</h2>
)}
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={setEditingNote}
onResize={refreshLayout}
/>
))}
</div>
</div>
)}
{editingNote && (
<NoteEditor note={editingNote} onClose={() => setEditingNote(null)} />
)}
<style jsx global>{`
.masonry-item {
display: block;
position: absolute;
z-index: 1;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
}
`}</style>
</div>
);
}

View File

@@ -0,0 +1,112 @@
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Archive,
ArchiveRestore,
MoreVertical,
Palette,
Pin,
Trash2,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { NOTE_COLORS } from "@/lib/types"
interface NoteActionsProps {
isPinned: boolean
isArchived: boolean
currentColor: string
onTogglePin: () => void
onToggleArchive: () => void
onColorChange: (color: string) => void
onDelete: () => void
className?: string
}
export function NoteActions({
isPinned,
isArchived,
currentColor,
onTogglePin,
onToggleArchive,
onColorChange,
onDelete,
className
}: NoteActionsProps) {
return (
<div
className={cn("flex items-center justify-end gap-1", className)}
onClick={(e) => e.stopPropagation()}
>
{/* Pin Button */}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={onTogglePin}
title={isPinned ? 'Unpin' : 'Pin'}
>
<Pin className={cn('h-4 w-4', isPinned && 'fill-current')} />
</Button>
{/* Color Palette */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" title="Change color">
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<div className="grid grid-cols-5 gap-2 p-2">
{Object.entries(NOTE_COLORS).map(([colorName, classes]) => (
<button
key={colorName}
className={cn(
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
classes.bg,
currentColor === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700'
)}
onClick={() => onColorChange(colorName)}
title={colorName}
/>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
{/* More Options */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={onToggleArchive}>
{isArchived ? (
<>
<ArchiveRestore className="h-4 w-4 mr-2" />
Unarchive
</>
) : (
<>
<Archive className="h-4 w-4 mr-2" />
Archive
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onDelete} className="text-red-600 dark:text-red-400">
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@@ -2,25 +2,7 @@
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Archive,
ArchiveRestore,
MoreVertical,
Palette,
Pin,
Tag,
Trash2,
Bell,
} from 'lucide-react'
import { Pin, Bell } from 'lucide-react'
import { useState } from 'react'
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote } from '@/app/actions/notes'
import { cn } from '@/lib/utils'
@@ -28,6 +10,9 @@ import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale'
import { MarkdownContent } from './markdown-content'
import { LabelBadge } from './label-badge'
import { NoteImages } from './note-images'
import { NoteChecklist } from './note-checklist'
import { NoteActions } from './note-actions'
interface NoteCardProps {
note: Note
@@ -116,62 +101,33 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
</h3>
)}
{/* Images */}
{note.images && note.images.length > 0 && (
<div className={cn(
"mb-3 -mx-4",
!note.title && "-mt-4"
)}>
{note.images.length === 1 ? (
<img
src={note.images[0]}
alt=""
className="w-full h-auto rounded-lg"
/>
) : note.images.length === 2 ? (
<div className="grid grid-cols-2 gap-2 px-4">
{note.images.map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
</div>
) : note.images.length === 3 ? (
<div className="grid grid-cols-2 gap-2 px-4">
<img
src={note.images[0]}
alt=""
className="col-span-2 w-full h-auto rounded-lg"
/>
{note.images.slice(1).map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
</div>
) : (
<div className="grid grid-cols-2 gap-2 px-4">
{note.images.slice(0, 4).map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
{note.images.length > 4 && (
<div className="absolute bottom-2 right-2 bg-black/70 text-white px-2 py-1 rounded text-xs">
+{note.images.length - 4}
</div>
{/* Images Component */}
<NoteImages images={note.images || []} title={note.title} />
{/* Link Previews */}
{note.links && note.links.length > 0 && (
<div className="flex flex-col gap-2 mb-2">
{note.links.map((link, idx) => (
<a
key={idx}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="block border rounded-md overflow-hidden bg-white/50 dark:bg-black/20 hover:bg-white/80 dark:hover:bg-black/40 transition-colors"
onClick={(e) => e.stopPropagation()}
>
{link.imageUrl && (
<div className="h-24 bg-cover bg-center" style={{ backgroundImage: `url(${link.imageUrl})` }} />
)}
</div>
)}
<div className="p-2">
<h4 className="font-medium text-xs truncate text-gray-900 dark:text-gray-100">{link.title || link.url}</h4>
{link.description && <p className="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mt-0.5">{link.description}</p>}
<span className="text-[10px] text-blue-500 mt-1 block">
{new URL(link.url).hostname}
</span>
</div>
</a>
))}
</div>
)}
@@ -187,33 +143,10 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
</p>
)
) : (
<div className="space-y-1">
{note.checkItems?.map((item) => (
<div
key={item.id}
className="flex items-start gap-2"
onClick={(e) => {
e.stopPropagation()
handleCheckItem(item.id)
}}
>
<Checkbox
checked={item.checked}
className="mt-0.5"
/>
<span
className={cn(
'text-sm',
item.checked
? 'line-through text-gray-500 dark:text-gray-500'
: 'text-gray-700 dark:text-gray-300'
)}
>
{item.text}
</span>
</div>
))}
</div>
<NoteChecklist
items={note.checkItems || []}
onToggleItem={handleCheckItem}
/>
)}
{/* Labels */}
@@ -230,76 +163,17 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver }: NoteCardProps
{formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: fr })}
</div>
{/* Action Bar - Shows on Hover */}
<div
className="absolute bottom-0 left-0 right-0 p-2 flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
{/* Pin Button */}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleTogglePin}
title={note.isPinned ? 'Unpin' : 'Pin'}
>
<Pin className={cn('h-4 w-4', note.isPinned && 'fill-current')} />
</Button>
{/* Color Palette */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" title="Change color">
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<div className="grid grid-cols-5 gap-2 p-2">
{Object.entries(NOTE_COLORS).map(([colorName, classes]) => (
<button
key={colorName}
className={cn(
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
classes.bg,
note.color === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700'
)}
onClick={() => handleColorChange(colorName)}
title={colorName}
/>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
{/* More Options */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleToggleArchive}>
{note.isArchived ? (
<>
<ArchiveRestore className="h-4 w-4 mr-2" />
Unarchive
</>
) : (
<>
<Archive className="h-4 w-4 mr-2" />
Archive
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDelete} className="text-red-600 dark:text-red-400">
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Action Bar Component */}
<NoteActions
isPinned={note.isPinned}
isArchived={note.isArchived}
currentColor={note.color}
onTogglePin={handleTogglePin}
onToggleArchive={handleToggleArchive}
onColorChange={handleColorChange}
onDelete={handleDelete}
className="absolute bottom-0 left-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity"
/>
</Card>
)
}
}

View File

@@ -0,0 +1,42 @@
import { CheckItem } from "@/lib/types"
import { Checkbox } from "@/components/ui/checkbox"
import { cn } from "@/lib/utils"
interface NoteChecklistProps {
items: CheckItem[]
onToggleItem: (itemId: string) => void
}
export function NoteChecklist({ items, onToggleItem }: NoteChecklistProps) {
if (!items || items.length === 0) return null
return (
<div className="space-y-1">
{items.map((item) => (
<div
key={item.id}
className="flex items-start gap-2"
onClick={(e) => {
e.stopPropagation()
onToggleItem(item.id)
}}
>
<Checkbox
checked={item.checked}
className="mt-0.5"
/>
<span
className={cn(
'text-sm',
item.checked
? 'line-through text-gray-500 dark:text-gray-500'
: 'text-gray-700 dark:text-gray-300'
)}
>
{item.text}
</span>
</div>
))}
</div>
)
}

View File

@@ -1,12 +1,13 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types'
import { useState, useRef } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata } from '@/lib/types'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@@ -17,13 +18,16 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { X, Plus, Palette, Image as ImageIcon, Bell, FileText, Eye } from 'lucide-react'
import { X, Plus, Palette, Image as ImageIcon, Bell, FileText, Eye, Link as LinkIcon } from 'lucide-react'
import { updateNote } from '@/app/actions/notes'
import { fetchLinkMetadata } from '@/app/actions/scrape'
import { cn } from '@/lib/utils'
import { useToast } from '@/components/ui/toast'
import { MarkdownContent } from './markdown-content'
import { LabelManager } from './label-manager'
import { LabelBadge } from './label-badge'
import { ReminderDialog } from './reminder-dialog'
import { EditorImages } from './editor-images'
interface NoteEditorProps {
note: Note
@@ -37,6 +41,7 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
const [labels, setLabels] = useState<string[]>(note.labels || [])
const [images, setImages] = useState<string[]>(note.images || [])
const [links, setLinks] = useState<LinkMetadata[]>(note.links || [])
const [newLabel, setNewLabel] = useState('')
const [color, setColor] = useState(note.color)
const [isSaving, setIsSaving] = useState(false)
@@ -46,69 +51,80 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
// Reminder state
const [showReminderDialog, setShowReminderDialog] = useState(false)
const [reminderDate, setReminderDate] = useState('')
const [reminderTime, setReminderTime] = useState('')
const [currentReminder, setCurrentReminder] = useState<Date | null>(note.reminder)
// Link state
const [showLinkDialog, setShowLinkDialog] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
Array.from(files).forEach(file => {
const reader = new FileReader()
reader.onloadend = () => {
setImages(prev => [...prev, reader.result as string])
for (const file of Array.from(files)) {
const formData = new FormData()
formData.append('file', file)
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
if (!response.ok) throw new Error('Upload failed')
const data = await response.json()
setImages(prev => [...prev, data.url])
} catch (error) {
console.error('Upload error:', error)
addToast(`Failed to upload ${file.name}`, 'error')
}
reader.readAsDataURL(file)
})
}
}
const handleRemoveImage = (index: number) => {
setImages(images.filter((_, i) => i !== index))
}
const handleReminderOpen = () => {
if (currentReminder) {
const date = new Date(currentReminder)
setReminderDate(date.toISOString().split('T')[0])
setReminderTime(date.toTimeString().slice(0, 5))
} else {
const tomorrow = new Date(Date.now() + 86400000)
setReminderDate(tomorrow.toISOString().split('T')[0])
setReminderTime('09:00')
const handleAddLink = async () => {
if (!linkUrl) return
setShowLinkDialog(false)
try {
const metadata = await fetchLinkMetadata(linkUrl)
if (metadata) {
setLinks(prev => [...prev, metadata])
addToast('Link added', 'success')
} else {
addToast('Could not fetch link metadata', 'warning')
setLinks(prev => [...prev, { url: linkUrl, title: linkUrl }])
}
} catch (error) {
console.error('Failed to add link:', error)
addToast('Failed to add link', 'error')
} finally {
setLinkUrl('')
}
setShowReminderDialog(true)
}
const handleReminderSave = () => {
if (!reminderDate || !reminderTime) {
addToast('Please enter date and time', 'warning')
return
}
const dateTimeString = `${reminderDate}T${reminderTime}`
const date = new Date(dateTimeString)
if (isNaN(date.getTime())) {
addToast('Invalid date or time', 'error')
return
}
const handleRemoveLink = (index: number) => {
setLinks(links.filter((_, i) => i !== index))
}
const handleReminderSave = (date: Date) => {
if (date < new Date()) {
addToast('Reminder must be in the future', 'error')
return
}
setCurrentReminder(date)
addToast(`Reminder set for ${date.toLocaleString()}`, 'success')
setShowReminderDialog(false)
}
const handleRemoveReminder = () => {
setCurrentReminder(null)
setShowReminderDialog(false)
addToast('Reminder removed', 'success')
}
@@ -121,6 +137,7 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
checkItems: note.type === 'checklist' ? checkItems : null,
labels,
images,
links,
color,
reminder: currentReminder,
isMarkdown,
@@ -158,13 +175,6 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
setCheckItems(items => items.filter(item => item.id !== id))
}
const handleAddLabel = () => {
if (newLabel.trim() && !labels.includes(newLabel.trim())) {
setLabels([...labels, newLabel.trim()])
setNewLabel('')
}
}
const handleRemoveLabel = (label: string) => {
setLabels(labels.filter(l => l !== label))
}
@@ -191,22 +201,30 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
/>
{/* Images */}
{images.length > 0 && (
<div className="flex flex-col gap-3 mb-4">
{images.map((img, idx) => (
<div key={idx} className="relative group">
<img
src={img}
alt=""
className="h-auto rounded-lg"
/>
<EditorImages images={images} onRemove={handleRemoveImage} />
{/* Link Previews */}
{links.length > 0 && (
<div className="flex flex-col gap-2">
{links.map((link, idx) => (
<div key={idx} className="relative group border rounded-lg overflow-hidden bg-white/50 dark:bg-black/20 flex">
{link.imageUrl && (
<div className="w-24 h-24 flex-shrink-0 bg-cover bg-center" style={{ backgroundImage: `url(${link.imageUrl})` }} />
)}
<div className="p-2 flex-1 min-w-0 flex flex-col justify-center">
<h4 className="font-medium text-sm truncate">{link.title || link.url}</h4>
{link.description && <p className="text-xs text-gray-500 truncate">{link.description}</p>}
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-blue-500 truncate hover:underline block mt-1">
{new URL(link.url).hostname}
</a>
</div>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-7 w-7 p-0 bg-white/90 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => handleRemoveImage(idx)}
className="absolute top-1 right-1 h-6 w-6 p-0 bg-white/50 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity rounded-full"
onClick={() => handleRemoveLink(idx)}
>
<X className="h-4 w-4" />
<X className="h-3 w-3" />
</Button>
</div>
))}
@@ -324,7 +342,7 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
<Button
variant="ghost"
size="sm"
onClick={handleReminderOpen}
onClick={() => setShowReminderDialog(true)}
title="Set reminder"
className={currentReminder ? "text-blue-600" : ""}
>
@@ -341,6 +359,16 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
<ImageIcon className="h-4 w-4" />
</Button>
{/* Add Link Button */}
<Button
variant="ghost"
size="sm"
onClick={() => setShowLinkDialog(true)}
title="Add Link"
>
<LinkIcon className="h-4 w-4" />
</Button>
{/* Color Picker */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -393,57 +421,84 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
/>
</DialogContent>
{/* Reminder Dialog */}
<Dialog open={showReminderDialog} onOpenChange={setShowReminderDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Set Reminder</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="reminder-date" className="text-sm font-medium">
Date
</label>
<Input
id="reminder-date"
type="date"
value={reminderDate}
onChange={(e) => setReminderDate(e.target.value)}
className="w-full"
/>
</div>
<div className="space-y-2">
<label htmlFor="reminder-time" className="text-sm font-medium">
Time
</label>
<Input
id="reminder-time"
type="time"
value={reminderTime}
onChange={(e) => setReminderTime(e.target.value)}
className="w-full"
/>
</div>
</div>
<div className="flex justify-between">
<div>
{currentReminder && (
<Button variant="outline" onClick={handleRemoveReminder}>
Remove Reminder
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={() => setShowReminderDialog(false)}>
Cancel
</Button>
<Button onClick={handleReminderSave}>
Set Reminder
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</Dialog>
)
}
<ReminderDialog
open={showReminderDialog}
onOpenChange={setShowReminderDialog}
currentReminder={currentReminder}
onSave={handleReminderSave}
onRemove={handleRemoveReminder}
/>
<Dialog open={showLinkDialog} onOpenChange={setShowLinkDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Link</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<Input
placeholder="https://example.com"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddLink()
}
}}
autoFocus
/>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowLinkDialog(false)}>
Cancel
</Button>
<Button onClick={handleAddLink}>
Add
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
)
}

View File

@@ -1,269 +0,0 @@
'use client'
import { Note } from '@/lib/types'
import { NoteCard } from './note-card'
import { useState, useMemo, useEffect } from 'react'
import { NoteEditor } from './note-editor'
import { reorderNotes, getNotes } from '@/app/actions/notes'
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
closestCenter,
PointerSensor,
} from '@dnd-kit/core'
import {
SortableContext,
rectSortingStrategy,
useSortable,
arrayMove,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useRouter } from 'next/navigation'
interface NoteGridProps {
notes: Note[]
}
function SortableNote({ note, onEdit }: { note: Note; onEdit: (note: Note) => void }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: note.id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 1000 : 1,
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
data-note-id={note.id}
data-draggable="true"
>
<NoteCard note={note} onEdit={onEdit} isDragging={isDragging} />
</div>
)
}
export function NoteGrid({ notes }: NoteGridProps) {
const router = useRouter()
const [editingNote, setEditingNote] = useState<Note | null>(null)
const [activeId, setActiveId] = useState<string | null>(null)
const [localPinnedNotes, setLocalPinnedNotes] = useState<Note[]>([])
const [localUnpinnedNotes, setLocalUnpinnedNotes] = useState<Note[]>([])
// Sync local state with props
useEffect(() => {
setLocalPinnedNotes(notes.filter(note => note.isPinned).sort((a, b) => a.order - b.order))
setLocalUnpinnedNotes(notes.filter(note => !note.isPinned).sort((a, b) => a.order - b.order))
}, [notes])
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(MouseSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 6,
},
})
)
const handleDragStart = (event: DragStartEvent) => {
console.log('[DND-DEBUG] Drag started:', {
activeId: event.active.id,
activeData: event.active.data.current
})
setActiveId(event.active.id as string)
}
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event
console.log('[DND-DEBUG] Drag ended:', {
activeId: active.id,
overId: over?.id,
hasOver: !!over
})
setActiveId(null)
if (!over || active.id === over.id) {
console.log('[DND-DEBUG] Drag cancelled: no valid drop target or same element')
return
}
const activeIdStr = active.id as string
const overIdStr = over.id as string
// Determine which section the dragged note belongs to
const isInPinned = localPinnedNotes.some(n => n.id === activeIdStr)
const targetIsInPinned = localPinnedNotes.some(n => n.id === overIdStr)
console.log('[DND-DEBUG] Section check:', {
activeIdStr,
overIdStr,
isInPinned,
targetIsInPinned,
pinnedNotesCount: localPinnedNotes.length,
unpinnedNotesCount: localUnpinnedNotes.length
})
// Only allow reordering within the same section
if (isInPinned !== targetIsInPinned) {
console.log('[DND-DEBUG] Drag cancelled: crossing sections (pinned/unpinned)')
return
}
if (isInPinned) {
// Reorder pinned notes
const oldIndex = localPinnedNotes.findIndex(n => n.id === activeIdStr)
const newIndex = localPinnedNotes.findIndex(n => n.id === overIdStr)
console.log('[DND-DEBUG] Pinned reorder:', { oldIndex, newIndex })
if (oldIndex !== -1 && newIndex !== -1) {
const newOrder = arrayMove(localPinnedNotes, oldIndex, newIndex)
setLocalPinnedNotes(newOrder)
console.log('[DND-DEBUG] Calling reorderNotes for pinned notes')
await reorderNotes(activeIdStr, overIdStr)
// Refresh notes from server to sync state
console.log('[DND-DEBUG] Refreshing notes from server after reorder')
await refreshNotesFromServer()
} else {
console.log('[DND-DEBUG] Invalid indices for pinned reorder')
}
} else {
// Reorder unpinned notes
const oldIndex = localUnpinnedNotes.findIndex(n => n.id === activeIdStr)
const newIndex = localUnpinnedNotes.findIndex(n => n.id === overIdStr)
console.log('[DND-DEBUG] Unpinned reorder:', { oldIndex, newIndex })
if (oldIndex !== -1 && newIndex !== -1) {
const newOrder = arrayMove(localUnpinnedNotes, oldIndex, newIndex)
setLocalUnpinnedNotes(newOrder)
console.log('[DND-DEBUG] Calling reorderNotes for unpinned notes')
await reorderNotes(activeIdStr, overIdStr)
// Refresh notes from server to sync state
console.log('[DND-DEBUG] Refreshing notes from server after reorder')
await refreshNotesFromServer()
} else {
console.log('[DND-DEBUG] Invalid indices for unpinned reorder')
}
}
}
// Function to refresh notes from server without full page reload
const refreshNotesFromServer = async () => {
console.log('[DND-DEBUG] Fetching fresh notes from server...')
const freshNotes = await getNotes()
console.log('[DND-DEBUG] Received fresh notes:', freshNotes.length)
// Update local state with fresh data
const pinned = freshNotes.filter(note => note.isPinned).sort((a, b) => a.order - b.order)
const unpinned = freshNotes.filter(note => !note.isPinned).sort((a, b) => a.order - b.order)
setLocalPinnedNotes(pinned)
setLocalUnpinnedNotes(unpinned)
console.log('[DND-DEBUG] Local state updated with fresh server data')
}
// Find active note from either section
const activeNote = activeId
? localPinnedNotes.find(n => n.id === activeId) || localUnpinnedNotes.find(n => n.id === activeId)
: null
return (
<>
<div className="space-y-8">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{localPinnedNotes.length > 0 && (
<div>
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
Pinned
</h2>
<SortableContext items={localPinnedNotes.map(n => n.id)} strategy={rectSortingStrategy}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 auto-rows-max">
{localPinnedNotes.map((note) => (
<SortableNote key={note.id} note={note} onEdit={setEditingNote} />
))}
</div>
</SortableContext>
</div>
)}
{localUnpinnedNotes.length > 0 && (
<div>
{localPinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
Others
</h2>
)}
<SortableContext items={localUnpinnedNotes.map(n => n.id)} strategy={rectSortingStrategy}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 auto-rows-max">
{localUnpinnedNotes.map((note) => (
<SortableNote key={note.id} note={note} onEdit={setEditingNote} />
))}
</div>
</SortableContext>
</div>
)}
<DragOverlay>
{activeNote ? (
<div className="opacity-90 rotate-2 scale-105 shadow-2xl">
<NoteCard
note={activeNote}
onEdit={() => {}}
isDragging={true}
/>
</div>
) : null}
</DragOverlay>
</DndContext>
{notes.length === 0 && (
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">No notes yet</p>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">Create your first note to get started</p>
</div>
)}
</div>
{editingNote && (
<NoteEditor note={editingNote} onClose={() => setEditingNote(null)} />
)}
</>
)
}

View File

@@ -0,0 +1,68 @@
import { cn } from "@/lib/utils"
interface NoteImagesProps {
images: string[]
title?: string | null
}
export function NoteImages({ images, title }: NoteImagesProps) {
if (!images || images.length === 0) return null
return (
<div className={cn(
"mb-3 -mx-4",
!title && "-mt-4"
)}>
{images.length === 1 ? (
<img
src={images[0]}
alt=""
className="w-full h-auto rounded-lg"
/>
) : images.length === 2 ? (
<div className="grid grid-cols-2 gap-2 px-4">
{images.map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
</div>
) : images.length === 3 ? (
<div className="grid grid-cols-2 gap-2 px-4">
<img
src={images[0]}
alt=""
className="col-span-2 w-full h-auto rounded-lg"
/>
{images.slice(1).map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
</div>
) : (
<div className="grid grid-cols-2 gap-2 px-4">
{images.slice(0, 4).map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
{images.length > 4 && (
<div className="absolute bottom-2 right-2 bg-black/70 text-white px-2 py-1 rounded text-xs">
+{images.length - 4}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -17,10 +17,12 @@ import {
Undo2,
Redo2,
FileText,
Eye
Eye,
Link as LinkIcon
} from 'lucide-react'
import { createNote } from '@/app/actions/notes'
import { CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types'
import { fetchLinkMetadata } from '@/app/actions/scrape'
import { CheckItem, NOTE_COLORS, NoteColor, LinkMetadata } from '@/lib/types'
import { Checkbox } from '@/components/ui/checkbox'
import {
Tooltip,
@@ -67,6 +69,7 @@ export function NoteInput() {
const [content, setContent] = useState('')
const [checkItems, setCheckItems] = useState<CheckItem[]>([])
const [images, setImages] = useState<string[]>([])
const [links, setLinks] = useState<LinkMetadata[]>([])
const [isMarkdown, setIsMarkdown] = useState(false)
const [showMarkdownPreview, setShowMarkdownPreview] = useState(false)
@@ -77,6 +80,8 @@ export function NoteInput() {
// Reminder dialog
const [showReminderDialog, setShowReminderDialog] = useState(false)
const [showLinkDialog, setShowLinkDialog] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const [reminderDate, setReminderDate] = useState('')
const [reminderTime, setReminderTime] = useState('')
const [currentReminder, setCurrentReminder] = useState<Date | null>(null)
@@ -148,7 +153,7 @@ export function NoteInput() {
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isExpanded, historyIndex, history])
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
@@ -156,32 +161,70 @@ export function NoteInput() {
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
const maxSize = 5 * 1024 * 1024 // 5MB
Array.from(files).forEach(file => {
for (const file of Array.from(files)) {
// Validation
if (!validTypes.includes(file.type)) {
addToast(`Invalid file type: ${file.name}. Only JPEG, PNG, GIF, and WebP allowed.`, 'error')
return
continue
}
if (file.size > maxSize) {
addToast(`File too large: ${file.name}. Maximum size is 5MB.`, 'error')
return
continue
}
const reader = new FileReader()
reader.onloadend = () => {
setImages([...images, reader.result as string])
// Upload to server
const formData = new FormData()
formData.append('file', file)
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
if (!response.ok) throw new Error('Upload failed')
const data = await response.json()
setImages(prev => [...prev, data.url])
} catch (error) {
console.error('Upload error:', error)
addToast(`Failed to upload ${file.name}`, 'error')
}
reader.onerror = () => {
addToast(`Failed to read file: ${file.name}`, 'error')
}
reader.readAsDataURL(file)
})
}
// Reset input
e.target.value = ''
}
const handleAddLink = async () => {
if (!linkUrl) return
// Optimistic add (or loading state)
setShowLinkDialog(false)
try {
const metadata = await fetchLinkMetadata(linkUrl)
if (metadata) {
setLinks(prev => [...prev, metadata])
addToast('Link added', 'success')
} else {
addToast('Could not fetch link metadata', 'warning')
// Fallback: just add the url as title
setLinks(prev => [...prev, { url: linkUrl, title: linkUrl }])
}
} catch (error) {
console.error('Failed to add link:', error)
addToast('Failed to add link', 'error')
} finally {
setLinkUrl('')
}
}
const handleRemoveLink = (index: number) => {
setLinks(links.filter((_, i) => i !== index))
}
const handleReminderOpen = () => {
const tomorrow = new Date(Date.now() + 86400000)
setReminderDate(tomorrow.toISOString().split('T')[0])
@@ -240,6 +283,7 @@ export function NoteInput() {
color,
isArchived,
images: images.length > 0 ? images : undefined,
links: links.length > 0 ? links : undefined,
reminder: currentReminder,
isMarkdown,
labels: selectedLabels.length > 0 ? selectedLabels : undefined,
@@ -250,6 +294,7 @@ export function NoteInput() {
setContent('')
setCheckItems([])
setImages([])
setLinks([])
setIsMarkdown(false)
setShowMarkdownPreview(false)
setHistory([{ title: '', content: '' }])
@@ -292,6 +337,7 @@ export function NoteInput() {
setContent('')
setCheckItems([])
setImages([])
setLinks([])
setIsMarkdown(false)
setShowMarkdownPreview(false)
setHistory([{ title: '', content: '' }])
@@ -369,17 +415,48 @@ export function NoteInput() {
</div>
)}
{/* Link Previews */}
{links.length > 0 && (
<div className="flex flex-col gap-2 mt-2">
{links.map((link, idx) => (
<div key={idx} className="relative group border rounded-lg overflow-hidden bg-gray-50 dark:bg-zinc-800/50 flex">
{link.imageUrl && (
<div className="w-24 h-24 flex-shrink-0 bg-cover bg-center" style={{ backgroundImage: `url(${link.imageUrl})` }} />
)}
<div className="p-2 flex-1 min-w-0 flex flex-col justify-center">
<h4 className="font-medium text-sm truncate">{link.title || link.url}</h4>
{link.description && <p className="text-xs text-gray-500 truncate">{link.description}</p>}
<a href={link.url} target="_blank" rel="noopener noreferrer" className="text-xs text-blue-500 truncate hover:underline block mt-1">
{new URL(link.url).hostname}
</a>
</div>
<Button
variant="ghost"
size="sm"
className="absolute top-1 right-1 h-6 w-6 p-0 bg-white/50 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity rounded-full"
onClick={() => handleRemoveLink(idx)}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
{type === 'text' ? (
<div className="space-y-2">
{/* Labels selector */}
<div className="flex items-center gap-2 mb-2">
<LabelSelector
selectedLabels={selectedLabels}
onLabelsChange={setSelectedLabels}
triggerLabel="Tags"
align="start"
/>
</div>
{/* Selected Labels Display */}
{selectedLabels.length > 0 && (
<div className="flex flex-wrap gap-1 mb-2">
{selectedLabels.map(label => (
<LabelBadge
key={label}
label={label}
onRemove={() => setSelectedLabels(prev => prev.filter(l => l !== label))}
/>
))}
</div>
)}
{/* Markdown toggle button */}
{isMarkdown && (
@@ -519,6 +596,28 @@ export function NoteInput() {
<TooltipContent>Collaborator</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setShowLinkDialog(true)}
title="Add Link"
>
<LinkIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Add Link</TooltipContent>
</Tooltip>
<LabelSelector
selectedLabels={selectedLabels}
onLabelsChange={setSelectedLabels}
triggerLabel=""
align="start"
/>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
@@ -676,6 +775,36 @@ export function NoteInput() {
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showLinkDialog} onOpenChange={setShowLinkDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Link</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<Input
placeholder="https://example.com"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddLink()
}
}}
autoFocus
/>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowLinkDialog(false)}>
Cancel
</Button>
<Button onClick={handleAddLink}>
Add
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,104 @@
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { register } from '@/app/actions/register';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import Link from 'next/link';
function RegisterButton() {
const { pending } = useFormStatus();
return (
<Button className="w-full mt-4" aria-disabled={pending}>
Register
</Button>
);
}
export function RegisterForm() {
const [errorMessage, dispatch] = useActionState(register, undefined);
return (
<form action={dispatch} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className="mb-3 text-2xl font-bold">
Create an account.
</h1>
<div className="w-full">
<div>
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="name"
>
Name
</label>
<div className="relative">
<Input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="name"
type="text"
name="name"
placeholder="Enter your name"
required
/>
</div>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="email"
>
Email
</label>
<div className="relative">
<Input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="email"
type="email"
name="email"
placeholder="Enter your email address"
required
/>
</div>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="password"
>
Password
</label>
<div className="relative">
<Input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="password"
type="password"
name="password"
placeholder="Enter password (min 6 chars)"
required
minLength={6}
/>
</div>
</div>
</div>
<RegisterButton />
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link href="/login" className="underline">
Log in
</Link>
</div>
</div>
</form>
);
}

View File

@@ -0,0 +1,102 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useState, useEffect } from "react"
interface ReminderDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
currentReminder: Date | null
onSave: (date: Date) => void
onRemove: () => void
}
export function ReminderDialog({
open,
onOpenChange,
currentReminder,
onSave,
onRemove
}: ReminderDialogProps) {
const [reminderDate, setReminderDate] = useState('')
const [reminderTime, setReminderTime] = useState('')
useEffect(() => {
if (open) {
if (currentReminder) {
const date = new Date(currentReminder)
setReminderDate(date.toISOString().split('T')[0])
setReminderTime(date.toTimeString().slice(0, 5))
} else {
const tomorrow = new Date(Date.now() + 86400000)
setReminderDate(tomorrow.toISOString().split('T')[0])
setReminderTime('09:00')
}
}
}, [open, currentReminder])
const handleSave = () => {
if (!reminderDate || !reminderTime) return
const dateTimeString = `${reminderDate}T${reminderTime}`
const date = new Date(dateTimeString)
if (!isNaN(date.getTime())) {
onSave(date)
onOpenChange(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Set Reminder</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="reminder-date" className="text-sm font-medium">
Date
</label>
<Input
id="reminder-date"
type="date"
value={reminderDate}
onChange={(e) => setReminderDate(e.target.value)}
className="w-full"
/>
</div>
<div className="space-y-2">
<label htmlFor="reminder-time" className="text-sm font-medium">
Time
</label>
<Input
id="reminder-time"
type="time"
value={reminderTime}
onChange={(e) => setReminderTime(e.target.value)}
className="w-full"
/>
</div>
</div>
<div className="flex justify-between">
<div>
{currentReminder && (
<Button variant="outline" onClick={() => { onRemove(); onOpenChange(false); }}>
Remove Reminder
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>
Set Reminder
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,110 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { usePathname, useSearchParams } from 'next/navigation'
import { cn } from '@/lib/utils'
import { StickyNote, Bell, Archive, Trash2, Tag, ChevronDown, ChevronUp } from 'lucide-react'
import { useLabels } from '@/context/LabelContext'
import { LabelManagementDialog } from './label-management-dialog'
import { LABEL_COLORS } from '@/lib/types'
export function Sidebar({ className }: { className?: string }) {
const pathname = usePathname()
const searchParams = useSearchParams()
const { labels, getLabelColor } = useLabels()
const [isLabelsExpanded, setIsLabelsExpanded] = useState(false)
const currentLabels = searchParams.get('labels')?.split(',').filter(Boolean) || []
const currentSearch = searchParams.get('search')
// Show first 5 labels by default, or all if expanded
const displayedLabels = isLabelsExpanded ? labels : labels.slice(0, 5)
const hasMoreLabels = labels.length > 5
const NavItem = ({ href, icon: Icon, label, active, onClick, iconColorClass }: any) => (
<Link
href={href}
onClick={onClick}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2",
active
? "bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
>
<Icon className={cn("h-5 w-5", active && "fill-current", !active && iconColorClass)} />
<span className="truncate">{label}</span>
</Link>
)
return (
<aside className={cn("w-[280px] flex-col gap-1 py-2 overflow-y-auto hidden md:flex", className)}>
<NavItem
href="/"
icon={StickyNote}
label="Notes"
active={pathname === '/' && currentLabels.length === 0 && !currentSearch}
/>
<NavItem
href="/reminders"
icon={Bell}
label="Reminders"
active={pathname === '/reminders'}
/>
<div className="my-2 px-4 flex items-center justify-between group">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</span>
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<LabelManagementDialog />
</div>
</div>
{displayedLabels.map(label => {
const colorName = getLabelColor(label.name)
const colorClass = LABEL_COLORS[colorName]?.icon
return (
<NavItem
key={label.id}
href={`/?labels=${encodeURIComponent(label.name)}`}
icon={Tag}
label={label.name}
active={currentLabels.includes(label.name)}
iconColorClass={colorClass}
/>
)
})}
{hasMoreLabels && (
<button
onClick={() => setIsLabelsExpanded(!isLabelsExpanded)}
className="flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2 w-full hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
>
{isLabelsExpanded ? (
<ChevronUp className="h-5 w-5" />
) : (
<ChevronDown className="h-5 w-5" />
)}
<span>{isLabelsExpanded ? 'Show less' : 'Show more'}</span>
</button>
)}
<div className="my-2 border-t border-gray-200 dark:border-zinc-800" />
<NavItem
href="/archive"
icon={Archive}
label="Archive"
active={pathname === '/archive'}
/>
<NavItem
href="/trash"
icon={Trash2}
label="Trash"
active={pathname === '/trash'}
/>
</aside>
)
}

View File

@@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -103,7 +103,7 @@ export function LabelProvider({ children }: { children: ReactNode }) {
}
const getLabelColorHelper = (name: string): LabelColorName => {
const label = labels.find(l => l.name === name)
const label = labels.find(l => l.name.toLowerCase() === name.toLowerCase())
return label?.color || 'gray'
}

View File

@@ -0,0 +1,23 @@
version: '3.8'
services:
keep-notes:
build:
context: .
dockerfile: Dockerfile
image: memento-app
container_name: memento-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
- DATABASE_URL=file:/app/prisma/dev.db
- NODE_ENV=production
volumes:
# Persist uploaded images
- ./public/uploads:/app/public/uploads
# Persist SQLite database
- ./prisma/dev.db:/app/prisma/dev.db
# Ensure the user inside docker has permissions to write to volumes
# You might need to adjust user IDs depending on your host system
# user: "1001:1001"

View File

@@ -0,0 +1,45 @@
'use client';
import { useState, useEffect } from 'react';
import { Note } from '@/lib/types';
import { useToast } from '@/components/ui/toast';
export function useReminderCheck(notes: Note[]) {
const [notifiedReminders, setNotifiedReminders] = useState<Set<string>>(new Set());
const { addToast } = useToast();
useEffect(() => {
const checkReminders = () => {
const now = new Date();
notes.forEach(note => {
if (!note.reminder) return;
const reminderDate = new Date(note.reminder);
// Check if reminder is due (within last minute or future)
// We only notify if it's due now or just passed, not old overdue ones (unless we want to catch up)
// Let's say: notify if reminder time is passed AND we haven't notified yet.
if (reminderDate <= now && !notifiedReminders.has(note.id)) {
// Play sound (optional)
// const audio = new Audio('/notification.mp3');
// audio.play().catch(e => console.log('Audio play failed', e));
addToast("🔔 Reminder: " + (note.title || "Untitled Note"), "info");
// Mark as notified in local state
setNotifiedReminders(prev => new Set(prev).add(note.id));
}
});
};
// Check immediately
checkReminders();
// Then check every 30 seconds
const interval = setInterval(checkReminders, 30000);
return () => clearInterval(interval);
}, [notes, notifiedReminders, addToast]);
}

View File

@@ -0,0 +1,33 @@
'use client';
import { useEffect, useRef } from 'react';
export function useResizeObserver(callback: (entry: ResizeObserverEntry) => void) {
const ref = useRef<HTMLElement>(null);
const frameId = useRef<number>(0);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver((entries) => {
// Cancel previous frame to avoid stacking updates
if (frameId.current) cancelAnimationFrame(frameId.current);
frameId.current = requestAnimationFrame(() => {
for (const entry of entries) {
callback(entry);
}
});
});
observer.observe(element);
return () => {
observer.disconnect();
if (frameId.current) cancelAnimationFrame(frameId.current);
};
}, [callback]);
return ref;
}

View File

@@ -1,7 +1,13 @@
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
return new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL || "file:D:/dev_new_pc/Keep/keep-notes/prisma/dev.db",
},
},
})
}
declare const globalThis: {

View File

@@ -13,6 +13,14 @@ export interface Label {
updatedAt: Date;
}
export interface LinkMetadata {
url: string;
title?: string;
description?: string;
imageUrl?: string;
siteName?: string;
}
export interface Note {
id: string;
title: string | null;
@@ -24,6 +32,7 @@ export interface Note {
checkItems: CheckItem[] | null;
labels: string[] | null;
images: string[] | null;
links: LinkMetadata[] | null;
reminder: Date | null;
reminderRecurrence: string | null;
reminderLocation: string | null;
@@ -39,16 +48,61 @@ export interface LabelWithColor {
}
export const LABEL_COLORS = {
gray: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-700 dark:text-gray-300', border: 'border-gray-300 dark:border-gray-600' },
red: { bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300', border: 'border-red-300 dark:border-red-600' },
orange: { bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-700 dark:text-orange-300', border: 'border-orange-300 dark:border-orange-600' },
yellow: { bg: 'bg-yellow-100 dark:bg-yellow-900/30', text: 'text-yellow-700 dark:text-yellow-300', border: 'border-yellow-300 dark:border-yellow-600' },
green: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300', border: 'border-green-300 dark:border-green-600' },
teal: { bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-700 dark:text-teal-300', border: 'border-teal-300 dark:border-teal-600' },
blue: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', border: 'border-blue-300 dark:border-blue-600' },
purple: { bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300', border: 'border-purple-300 dark:border-purple-600' },
pink: { bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300', border: 'border-pink-300 dark:border-pink-600' },
}
gray: {
bg: 'bg-zinc-100 dark:bg-zinc-800',
text: 'text-zinc-700 dark:text-zinc-300',
border: 'border-zinc-200 dark:border-zinc-700',
icon: 'text-zinc-500 dark:text-zinc-400'
},
red: {
bg: 'bg-rose-100 dark:bg-rose-900/40',
text: 'text-rose-700 dark:text-rose-300',
border: 'border-rose-200 dark:border-rose-800',
icon: 'text-rose-500 dark:text-rose-400'
},
orange: {
bg: 'bg-orange-100 dark:bg-orange-900/40',
text: 'text-orange-700 dark:text-orange-300',
border: 'border-orange-200 dark:border-orange-800',
icon: 'text-orange-500 dark:text-orange-400'
},
yellow: {
bg: 'bg-amber-100 dark:bg-amber-900/40',
text: 'text-amber-700 dark:text-amber-300',
border: 'border-amber-200 dark:border-amber-800',
icon: 'text-amber-500 dark:text-amber-400'
},
green: {
bg: 'bg-emerald-100 dark:bg-emerald-900/40',
text: 'text-emerald-700 dark:text-emerald-300',
border: 'border-emerald-200 dark:border-emerald-800',
icon: 'text-emerald-500 dark:text-emerald-400'
},
teal: {
bg: 'bg-teal-100 dark:bg-teal-900/40',
text: 'text-teal-700 dark:text-teal-300',
border: 'border-teal-200 dark:border-teal-800',
icon: 'text-teal-500 dark:text-teal-400'
},
blue: {
bg: 'bg-sky-100 dark:bg-sky-900/40',
text: 'text-sky-700 dark:text-sky-300',
border: 'border-sky-200 dark:border-sky-800',
icon: 'text-sky-500 dark:text-sky-400'
},
purple: {
bg: 'bg-violet-100 dark:bg-violet-900/40',
text: 'text-violet-700 dark:text-violet-300',
border: 'border-violet-200 dark:border-violet-800',
icon: 'text-violet-500 dark:text-violet-400'
},
pink: {
bg: 'bg-fuchsia-100 dark:bg-fuchsia-900/40',
text: 'text-fuchsia-700 dark:text-fuchsia-300',
border: 'border-fuchsia-200 dark:border-fuchsia-800',
icon: 'text-fuchsia-500 dark:text-fuchsia-400'
},
} as const;
export type LabelColorName = keyof typeof LABEL_COLORS

9
keep-notes/middleware.ts Normal file
View File

@@ -0,0 +1,9 @@
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\.png$).*)'],
};

View File

@@ -1,7 +1,11 @@
import type { NextConfig } from "next";
import path from "path";
const nextConfig: NextConfig = {
/* config options here */
turbopack: {
// Set root to parent directory to support monorepo workspace structure
root: path.resolve(__dirname, ".."),
},
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"@libsql/client": "^0.15.15",
"@prisma/adapter-better-sqlite3": "^7.2.0",
"@prisma/adapter-libsql": "^7.2.0",
"@prisma/client": "5.22.0",
"@prisma/client": "^5.22.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -26,26 +26,32 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.5.0",
"cheerio": "^1.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"lucide-react": "^0.562.0",
"muuri": "^0.9.5",
"next": "16.1.1",
"next-auth": "^5.0.0-beta.30",
"prisma": "5.22.0",
"prisma": "^5.22.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-grid-layout": "^2.2.2",
"react-markdown": "^10.1.0",
"react-masonry-css": "^1.0.16",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.4.0"
"tailwind-merge": "^3.4.0",
"web-animations-js": "^2.3.2",
"zod": "^4.3.5"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20",
"@types/react": "^19",

View File

@@ -1,478 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e3]:
- link "Memento" [ref=e4] [cursor=pointer]:
- /url: /
- img [ref=e5]
- generic [ref=e8]: Memento
- generic [ref=e10]:
- img [ref=e11]
- textbox "Search notes..." [ref=e14]
- button [ref=e15]:
- img
- navigation [ref=e16]:
- link "Notes" [ref=e17] [cursor=pointer]:
- /url: /
- img [ref=e18]
- text: Notes
- link "Archive" [ref=e21] [cursor=pointer]:
- /url: /archive
- img [ref=e22]
- text: Archive
- main [ref=e25]:
- generic [ref=e27]:
- textbox "Take a note..." [ref=e28]
- button "New checklist" [ref=e29]:
- img
- generic [ref=e30]:
- generic [ref=e31]:
- heading "Pinned" [level=2] [ref=e32]
- button "Updated Note avec image il y a environ 8 heures" [ref=e34]:
- generic [ref=e35]:
- img [ref=e36]
- heading "Updated" [level=3] [ref=e38]
- paragraph [ref=e39]: Note avec image
- generic [ref=e40]: il y a environ 8 heures
- generic [ref=e41]:
- button "Unpin" [ref=e42]:
- img
- button "Change color" [ref=e43]:
- img
- button [ref=e44]:
- img
- generic [ref=e45]:
- heading "Others" [level=2] [ref=e46]
- generic [ref=e47]:
- button "test-1767557334587-Note 4 test-1767557334587-Content 4 il y a moins dune minute" [ref=e48]:
- generic [ref=e49]:
- heading "test-1767557334587-Note 4" [level=3] [ref=e50]
- paragraph [ref=e51]: test-1767557334587-Content 4
- generic [ref=e52]: il y a moins dune minute
- generic [ref=e53]:
- button "Pin" [ref=e54]:
- img
- button "Change color" [ref=e55]:
- img
- button [ref=e56]:
- img
- button "test-1767557334587-Note 3 test-1767557334587-Content 3 il y a moins dune minute" [ref=e57]:
- generic [ref=e58]:
- heading "test-1767557334587-Note 3" [level=3] [ref=e59]
- paragraph [ref=e60]: test-1767557334587-Content 3
- generic [ref=e61]: il y a moins dune minute
- generic [ref=e62]:
- button "Pin" [ref=e63]:
- img
- button "Change color" [ref=e64]:
- img
- button [ref=e65]:
- img
- button "test-1767557334587-Note 2 test-1767557334587-Content 2 il y a moins dune minute" [ref=e66]:
- generic [ref=e67]:
- heading "test-1767557334587-Note 2" [level=3] [ref=e68]
- paragraph [ref=e69]: test-1767557334587-Content 2
- generic [ref=e70]: il y a moins dune minute
- generic [ref=e71]:
- button "Pin" [ref=e72]:
- img
- button "Change color" [ref=e73]:
- img
- button [ref=e74]:
- img
- button "test-1767557334587-Note 1 test-1767557334587-Content 1 il y a moins dune minute" [ref=e75]:
- generic [ref=e76]:
- heading "test-1767557334587-Note 1" [level=3] [ref=e77]
- paragraph [ref=e78]: test-1767557334587-Content 1
- generic [ref=e79]: il y a moins dune minute
- generic [ref=e80]:
- button "Pin" [ref=e81]:
- img
- button "Change color" [ref=e82]:
- img
- button [ref=e83]:
- img
- button "test-1767557330820-Note 4 test-1767557330820-Content 4 il y a moins dune minute" [ref=e84]:
- generic [ref=e85]:
- heading "test-1767557330820-Note 4" [level=3] [ref=e86]
- paragraph [ref=e87]: test-1767557330820-Content 4
- generic [ref=e88]: il y a moins dune minute
- generic [ref=e89]:
- button "Pin" [ref=e90]:
- img
- button "Change color" [ref=e91]:
- img
- button [ref=e92]:
- img
- button "test-1767557330820-Note 3 test-1767557330820-Content 3 il y a moins dune minute" [ref=e93]:
- generic [ref=e94]:
- heading "test-1767557330820-Note 3" [level=3] [ref=e95]
- paragraph [ref=e96]: test-1767557330820-Content 3
- generic [ref=e97]: il y a moins dune minute
- generic [ref=e98]:
- button "Pin" [ref=e99]:
- img
- button "Change color" [ref=e100]:
- img
- button [ref=e101]:
- img
- button "test-1767557330820-Note 2 test-1767557330820-Content 2 il y a moins dune minute" [ref=e102]:
- generic [ref=e103]:
- heading "test-1767557330820-Note 2" [level=3] [ref=e104]
- paragraph [ref=e105]: test-1767557330820-Content 2
- generic [ref=e106]: il y a moins dune minute
- generic [ref=e107]:
- button "Pin" [ref=e108]:
- img
- button "Change color" [ref=e109]:
- img
- button [ref=e110]:
- img
- button "test-1767557330820-Note 1 test-1767557330820-Content 1 il y a moins dune minute" [ref=e111]:
- generic [ref=e112]:
- heading "test-1767557330820-Note 1" [level=3] [ref=e113]
- paragraph [ref=e114]: test-1767557330820-Content 1
- generic [ref=e115]: il y a moins dune minute
- generic [ref=e116]:
- button "Pin" [ref=e117]:
- img
- button "Change color" [ref=e118]:
- img
- button [ref=e119]:
- img
- button "test-1767557327567-Note 4 test-1767557327567-Content 4 il y a moins dune minute" [ref=e120]:
- generic [ref=e121]:
- heading "test-1767557327567-Note 4" [level=3] [ref=e122]
- paragraph [ref=e123]: test-1767557327567-Content 4
- generic [ref=e124]: il y a moins dune minute
- generic [ref=e125]:
- button "Pin" [ref=e126]:
- img
- button "Change color" [ref=e127]:
- img
- button [ref=e128]:
- img
- button "test-1767557327567-Note 3 test-1767557327567-Content 3 il y a moins dune minute" [ref=e129]:
- generic [ref=e130]:
- heading "test-1767557327567-Note 3" [level=3] [ref=e131]
- paragraph [ref=e132]: test-1767557327567-Content 3
- generic [ref=e133]: il y a moins dune minute
- generic [ref=e134]:
- button "Pin" [ref=e135]:
- img
- button "Change color" [ref=e136]:
- img
- button [ref=e137]:
- img
- button "test-1767557327567-Note 2 test-1767557327567-Content 2 il y a moins dune minute" [ref=e138]:
- generic [ref=e139]:
- heading "test-1767557327567-Note 2" [level=3] [ref=e140]
- paragraph [ref=e141]: test-1767557327567-Content 2
- generic [ref=e142]: il y a moins dune minute
- generic [ref=e143]:
- button "Pin" [ref=e144]:
- img
- button "Change color" [ref=e145]:
- img
- button [ref=e146]:
- img
- button "test-1767557327567-Note 1 test-1767557327567-Content 1 il y a moins dune minute" [ref=e147]:
- generic [ref=e148]:
- heading "test-1767557327567-Note 1" [level=3] [ref=e149]
- paragraph [ref=e150]: test-1767557327567-Content 1
- generic [ref=e151]: il y a moins dune minute
- generic [ref=e152]:
- button "Pin" [ref=e153]:
- img
- button "Change color" [ref=e154]:
- img
- button [ref=e155]:
- img
- button "test-1767557324248-Note 4 test-1767557324248-Content 4 il y a moins dune minute" [ref=e156]:
- generic [ref=e157]:
- heading "test-1767557324248-Note 4" [level=3] [ref=e158]
- paragraph [ref=e159]: test-1767557324248-Content 4
- generic [ref=e160]: il y a moins dune minute
- generic [ref=e161]:
- button "Pin" [ref=e162]:
- img
- button "Change color" [ref=e163]:
- img
- button [ref=e164]:
- img
- button "test-1767557324248-Note 3 test-1767557324248-Content 3 il y a moins dune minute" [ref=e165]:
- generic [ref=e166]:
- heading "test-1767557324248-Note 3" [level=3] [ref=e167]
- paragraph [ref=e168]: test-1767557324248-Content 3
- generic [ref=e169]: il y a moins dune minute
- generic [ref=e170]:
- button "Pin" [ref=e171]:
- img
- button "Change color" [ref=e172]:
- img
- button [ref=e173]:
- img
- button "test-1767557324248-Note 2 test-1767557324248-Content 2 il y a moins dune minute" [ref=e174]:
- generic [ref=e175]:
- heading "test-1767557324248-Note 2" [level=3] [ref=e176]
- paragraph [ref=e177]: test-1767557324248-Content 2
- generic [ref=e178]: il y a moins dune minute
- generic [ref=e179]:
- button "Pin" [ref=e180]:
- img
- button "Change color" [ref=e181]:
- img
- button [ref=e182]:
- img
- button "test-1767557324248-Note 1 test-1767557324248-Content 1 il y a moins dune minute" [ref=e183]:
- generic [ref=e184]:
- heading "test-1767557324248-Note 1" [level=3] [ref=e185]
- paragraph [ref=e186]: test-1767557324248-Content 1
- generic [ref=e187]: il y a moins dune minute
- generic [ref=e188]:
- button "Pin" [ref=e189]:
- img
- button "Change color" [ref=e190]:
- img
- button [ref=e191]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e192]:
- generic [ref=e193]:
- heading "Test Note for Reminder" [level=3] [ref=e194]
- paragraph [ref=e195]: This note will have a reminder
- generic [ref=e196]: il y a 26 minutes
- generic [ref=e197]:
- button "Pin" [ref=e198]:
- img
- button "Change color" [ref=e199]:
- img
- button [ref=e200]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e201]:
- generic [ref=e202]:
- heading "Test Note for Reminder" [level=3] [ref=e203]
- paragraph [ref=e204]: This note will have a reminder
- generic [ref=e205]: il y a 26 minutes
- generic [ref=e206]:
- button "Pin" [ref=e207]:
- img
- button "Change color" [ref=e208]:
- img
- button [ref=e209]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e210]:
- generic [ref=e211]:
- heading "Test Note for Reminder" [level=3] [ref=e212]
- paragraph [ref=e213]: This note will have a reminder
- generic [ref=e214]: il y a 26 minutes
- generic [ref=e215]:
- button "Pin" [ref=e216]:
- img
- button "Change color" [ref=e217]:
- img
- button [ref=e218]:
- img
- button "Test note il y a 26 minutes" [ref=e219]:
- generic [ref=e220]:
- paragraph [ref=e221]: Test note
- generic [ref=e222]: il y a 26 minutes
- generic [ref=e223]:
- button "Pin" [ref=e224]:
- img
- button "Change color" [ref=e225]:
- img
- button [ref=e226]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e227]:
- generic [ref=e228]:
- heading "Test Note for Reminder" [level=3] [ref=e229]
- paragraph [ref=e230]: This note will have a reminder
- generic [ref=e231]: il y a 26 minutes
- generic [ref=e232]:
- button "Pin" [ref=e233]:
- img
- button "Change color" [ref=e234]:
- img
- button [ref=e235]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e236]:
- generic [ref=e237]:
- heading "Test Note for Reminder" [level=3] [ref=e238]
- paragraph [ref=e239]: This note will have a reminder
- generic [ref=e240]: il y a 26 minutes
- generic [ref=e241]:
- button "Pin" [ref=e242]:
- img
- button "Change color" [ref=e243]:
- img
- button [ref=e244]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e245]:
- generic [ref=e246]:
- heading "Test Note for Reminder" [level=3] [ref=e247]
- paragraph [ref=e248]: This note will have a reminder
- generic [ref=e249]: il y a 26 minutes
- generic [ref=e250]:
- button "Pin" [ref=e251]:
- img
- button "Change color" [ref=e252]:
- img
- button [ref=e253]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e254]:
- generic [ref=e255]:
- img [ref=e256]
- heading "Test Note for Reminder" [level=3] [ref=e259]
- paragraph [ref=e260]: This note will have a reminder
- generic [ref=e261]: il y a 26 minutes
- generic [ref=e262]:
- button "Pin" [ref=e263]:
- img
- button "Change color" [ref=e264]:
- img
- button [ref=e265]:
- img
- button "test sample file il y a environ 5 heures" [ref=e266]:
- generic [ref=e267]:
- heading "test" [level=3] [ref=e268]
- paragraph [ref=e270]: sample file
- generic [ref=e271]: il y a environ 5 heures
- generic [ref=e272]:
- button "Pin" [ref=e273]:
- img
- button "Change color" [ref=e274]:
- img
- button [ref=e275]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d''un framework pour l''IA - Optimisation de l''entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l''inférence en temps réel Tags: framework, mlops, gpu, ai, tech tech ai framework mlops gpu il y a environ 5 heures" [ref=e276]':
- generic [ref=e277]:
- heading "New AI Framework Released" [level=3] [ref=e278]
- paragraph [ref=e279]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d'un framework pour l'IA - Optimisation de l'entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l'inférence en temps réel Tags: framework, mlops, gpu, ai, tech"
- generic [ref=e280]:
- generic [ref=e281]: tech
- generic [ref=e282]: ai
- generic [ref=e283]: framework
- generic [ref=e284]: mlops
- generic [ref=e285]: gpu
- generic [ref=e286]: il y a environ 5 heures
- generic [ref=e287]:
- button "Pin" [ref=e288]:
- img
- button "Change color" [ref=e289]:
- img
- button [ref=e290]:
- img
- button "Test Image API Note avec image il y a environ 8 heures" [ref=e291]:
- generic [ref=e292]:
- heading "Test Image API" [level=3] [ref=e293]
- paragraph [ref=e295]: Note avec image
- generic [ref=e296]: il y a environ 8 heures
- generic [ref=e297]:
- button "Pin" [ref=e298]:
- img
- button "Change color" [ref=e299]:
- img
- button [ref=e300]:
- img
- button "Test Markdown Titre Modifié Sous-titre édité Liste modifiée 1 Liste modifiée 2 Nouvelle liste 3 Texte gras modifié et italique édité console.log(\"Code modifié avec succès!\") il y a environ 5 heures" [ref=e301]:
- generic [ref=e302]:
- heading "Test Markdown" [level=3] [ref=e303]
- generic [ref=e305]:
- heading "Titre Modifié" [level=1] [ref=e306]
- heading "Sous-titre édité" [level=2] [ref=e307]
- list [ref=e308]:
- listitem [ref=e309]: Liste modifiée 1
- listitem [ref=e310]: Liste modifiée 2
- listitem [ref=e311]: Nouvelle liste 3
- paragraph [ref=e312]:
- strong [ref=e313]: Texte gras modifié
- text: et
- emphasis [ref=e314]: italique édité
- code [ref=e316]: console.log("Code modifié avec succès!")
- generic [ref=e317]: il y a environ 5 heures
- generic [ref=e318]:
- button "Pin" [ref=e319]:
- img
- button "Change color" [ref=e320]:
- img
- button [ref=e321]:
- img
- button "Test Image Avec image il y a environ 8 heures" [ref=e322]:
- generic [ref=e323]:
- heading "Test Image" [level=3] [ref=e324]
- paragraph [ref=e325]: Avec image
- generic [ref=e326]: il y a environ 8 heures
- generic [ref=e327]:
- button "Pin" [ref=e328]:
- img
- button "Change color" [ref=e329]:
- img
- button [ref=e330]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu tech ai framework mlops gpu il y a environ 6 heures" [ref=e331]':
- generic [ref=e332]:
- heading "New AI Framework Released" [level=3] [ref=e333]
- paragraph [ref=e334]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu"
- generic [ref=e335]:
- generic [ref=e336]: tech
- generic [ref=e337]: ai
- generic [ref=e338]: framework
- generic [ref=e339]: mlops
- generic [ref=e340]: gpu
- generic [ref=e341]: il y a environ 6 heures
- generic [ref=e342]:
- button "Pin" [ref=e343]:
- img
- button "Change color" [ref=e344]:
- img
- button [ref=e345]:
- img
- button "Test Note This is my first note to test the Google Keep clone! il y a environ 10 heures" [ref=e346]:
- generic [ref=e347]:
- img [ref=e348]
- heading "Test Note" [level=3] [ref=e351]
- paragraph [ref=e352]: This is my first note to test the Google Keep clone!
- generic [ref=e353]: il y a environ 10 heures
- generic [ref=e354]:
- button "Pin" [ref=e355]:
- img
- button "Change color" [ref=e356]:
- img
- button [ref=e357]:
- img
- button "Titre Modifié Contenu modifié avec succès! il y a environ 5 heures" [ref=e358]:
- generic [ref=e359]:
- heading "Titre Modifié" [level=3] [ref=e360]
- paragraph [ref=e361]: Contenu modifié avec succès!
- generic [ref=e362]: il y a environ 5 heures
- generic [ref=e363]:
- button "Pin" [ref=e364]:
- img
- button "Change color" [ref=e365]:
- img
- button [ref=e366]:
- img
- status [ref=e367]
- generic [ref=e368]:
- generic [ref=e369]:
- generic [ref=e370]: Note created successfully
- button [ref=e371]:
- img [ref=e372]
- generic [ref=e375]:
- generic [ref=e376]: Note created successfully
- button [ref=e377]:
- img [ref=e378]
- generic [ref=e381]:
- generic [ref=e382]: Note created successfully
- button [ref=e383]:
- img [ref=e384]
- generic [ref=e387]:
- generic [ref=e388]: Note created successfully
- button [ref=e389]:
- img [ref=e390]
- button "Open Next.js Dev Tools" [ref=e398] [cursor=pointer]:
- img [ref=e399]
- alert [ref=e402]
```

View File

@@ -1,509 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e3]:
- link "Memento" [ref=e4] [cursor=pointer]:
- /url: /
- img [ref=e5]
- generic [ref=e8]: Memento
- generic [ref=e10]:
- img [ref=e11]
- textbox "Search notes..." [ref=e14]
- button [ref=e15]:
- img
- navigation [ref=e16]:
- link "Notes" [ref=e17] [cursor=pointer]:
- /url: /
- img [ref=e18]
- text: Notes
- link "Archive" [ref=e21] [cursor=pointer]:
- /url: /archive
- img [ref=e22]
- text: Archive
- main [ref=e25]:
- generic [ref=e27]:
- textbox "Take a note..." [ref=e28]
- button "New checklist" [ref=e29]:
- img
- generic [ref=e30]:
- generic [ref=e31]:
- heading "Pinned" [level=2] [ref=e32]
- button "Updated Note avec image il y a environ 8 heures" [ref=e34]:
- generic [ref=e35]:
- img [ref=e36]
- heading "Updated" [level=3] [ref=e38]
- paragraph [ref=e39]: Note avec image
- generic [ref=e40]: il y a environ 8 heures
- generic [ref=e41]:
- button "Unpin" [ref=e42]:
- img
- button "Change color" [ref=e43]:
- img
- button [ref=e44]:
- img
- generic [ref=e45]:
- heading "Others" [level=2] [ref=e46]
- generic [ref=e47]:
- button "test-1767557339218-Note 4 test-1767557339218-Content 4 il y a moins dune minute" [ref=e48]:
- generic [ref=e49]:
- heading "test-1767557339218-Note 4" [level=3] [ref=e50]
- paragraph [ref=e51]: test-1767557339218-Content 4
- generic [ref=e52]: il y a moins dune minute
- generic [ref=e53]:
- button "Pin" [ref=e54]:
- img
- button "Change color" [ref=e55]:
- img
- button [ref=e56]:
- img
- button "test-1767557339218-Note 3 test-1767557339218-Content 3 il y a moins dune minute" [ref=e57]:
- generic [ref=e58]:
- heading "test-1767557339218-Note 3" [level=3] [ref=e59]
- paragraph [ref=e60]: test-1767557339218-Content 3
- generic [ref=e61]: il y a moins dune minute
- generic [ref=e62]:
- button "Pin" [ref=e63]:
- img
- button "Change color" [ref=e64]:
- img
- button [ref=e65]:
- img
- button "test-1767557339218-Note 2 test-1767557339218-Content 2 il y a moins dune minute" [ref=e66]:
- generic [ref=e67]:
- heading "test-1767557339218-Note 2" [level=3] [ref=e68]
- paragraph [ref=e69]: test-1767557339218-Content 2
- generic [ref=e70]: il y a moins dune minute
- generic [ref=e71]:
- button "Pin" [ref=e72]:
- img
- button "Change color" [ref=e73]:
- img
- button [ref=e74]:
- img
- button "test-1767557339218-Note 1 test-1767557339218-Content 1 il y a moins dune minute" [ref=e75]:
- generic [ref=e76]:
- heading "test-1767557339218-Note 1" [level=3] [ref=e77]
- paragraph [ref=e78]: test-1767557339218-Content 1
- generic [ref=e79]: il y a moins dune minute
- generic [ref=e80]:
- button "Pin" [ref=e81]:
- img
- button "Change color" [ref=e82]:
- img
- button [ref=e83]:
- img
- button "test-1767557334587-Note 4 test-1767557334587-Content 4 il y a moins dune minute" [ref=e84]:
- generic [ref=e85]:
- heading "test-1767557334587-Note 4" [level=3] [ref=e86]
- paragraph [ref=e87]: test-1767557334587-Content 4
- generic [ref=e88]: il y a moins dune minute
- generic [ref=e89]:
- button "Pin" [ref=e90]:
- img
- button "Change color" [ref=e91]:
- img
- button [ref=e92]:
- img
- button "test-1767557334587-Note 3 test-1767557334587-Content 3 il y a moins dune minute" [ref=e93]:
- generic [ref=e94]:
- heading "test-1767557334587-Note 3" [level=3] [ref=e95]
- paragraph [ref=e96]: test-1767557334587-Content 3
- generic [ref=e97]: il y a moins dune minute
- generic [ref=e98]:
- button "Pin" [ref=e99]:
- img
- button "Change color" [ref=e100]:
- img
- button [ref=e101]:
- img
- button "test-1767557334587-Note 2 test-1767557334587-Content 2 il y a moins dune minute" [ref=e102]:
- generic [ref=e103]:
- heading "test-1767557334587-Note 2" [level=3] [ref=e104]
- paragraph [ref=e105]: test-1767557334587-Content 2
- generic [ref=e106]: il y a moins dune minute
- generic [ref=e107]:
- button "Pin" [ref=e108]:
- img
- button "Change color" [ref=e109]:
- img
- button [ref=e110]:
- img
- button "test-1767557334587-Note 1 test-1767557334587-Content 1 il y a moins dune minute" [ref=e111]:
- generic [ref=e112]:
- heading "test-1767557334587-Note 1" [level=3] [ref=e113]
- paragraph [ref=e114]: test-1767557334587-Content 1
- generic [ref=e115]: il y a moins dune minute
- generic [ref=e116]:
- button "Pin" [ref=e117]:
- img
- button "Change color" [ref=e118]:
- img
- button [ref=e119]:
- img
- button "test-1767557330820-Note 4 test-1767557330820-Content 4 il y a moins dune minute" [ref=e120]:
- generic [ref=e121]:
- heading "test-1767557330820-Note 4" [level=3] [ref=e122]
- paragraph [ref=e123]: test-1767557330820-Content 4
- generic [ref=e124]: il y a moins dune minute
- generic [ref=e125]:
- button "Pin" [ref=e126]:
- img
- button "Change color" [ref=e127]:
- img
- button [ref=e128]:
- img
- button "test-1767557330820-Note 3 test-1767557330820-Content 3 il y a moins dune minute" [ref=e129]:
- generic [ref=e130]:
- heading "test-1767557330820-Note 3" [level=3] [ref=e131]
- paragraph [ref=e132]: test-1767557330820-Content 3
- generic [ref=e133]: il y a moins dune minute
- generic [ref=e134]:
- button "Pin" [ref=e135]:
- img
- button "Change color" [ref=e136]:
- img
- button [ref=e137]:
- img
- button "test-1767557330820-Note 2 test-1767557330820-Content 2 il y a moins dune minute" [ref=e138]:
- generic [ref=e139]:
- heading "test-1767557330820-Note 2" [level=3] [ref=e140]
- paragraph [ref=e141]: test-1767557330820-Content 2
- generic [ref=e142]: il y a moins dune minute
- generic [ref=e143]:
- button "Pin" [ref=e144]:
- img
- button "Change color" [ref=e145]:
- img
- button [ref=e146]:
- img
- button "test-1767557330820-Note 1 test-1767557330820-Content 1 il y a moins dune minute" [ref=e147]:
- generic [ref=e148]:
- heading "test-1767557330820-Note 1" [level=3] [ref=e149]
- paragraph [ref=e150]: test-1767557330820-Content 1
- generic [ref=e151]: il y a moins dune minute
- generic [ref=e152]:
- button "Pin" [ref=e153]:
- img
- button "Change color" [ref=e154]:
- img
- button [ref=e155]:
- img
- button "test-1767557327567-Note 4 test-1767557327567-Content 4 il y a moins dune minute" [ref=e156]:
- generic [ref=e157]:
- heading "test-1767557327567-Note 4" [level=3] [ref=e158]
- paragraph [ref=e159]: test-1767557327567-Content 4
- generic [ref=e160]: il y a moins dune minute
- generic [ref=e161]:
- button "Pin" [ref=e162]:
- img
- button "Change color" [ref=e163]:
- img
- button [ref=e164]:
- img
- button "test-1767557327567-Note 3 test-1767557327567-Content 3 il y a moins dune minute" [ref=e165]:
- generic [ref=e166]:
- heading "test-1767557327567-Note 3" [level=3] [ref=e167]
- paragraph [ref=e168]: test-1767557327567-Content 3
- generic [ref=e169]: il y a moins dune minute
- generic [ref=e170]:
- button "Pin" [ref=e171]:
- img
- button "Change color" [ref=e172]:
- img
- button [ref=e173]:
- img
- button "test-1767557327567-Note 2 test-1767557327567-Content 2 il y a moins dune minute" [ref=e174]:
- generic [ref=e175]:
- heading "test-1767557327567-Note 2" [level=3] [ref=e176]
- paragraph [ref=e177]: test-1767557327567-Content 2
- generic [ref=e178]: il y a moins dune minute
- generic [ref=e179]:
- button "Pin" [ref=e180]:
- img
- button "Change color" [ref=e181]:
- img
- button [ref=e182]:
- img
- button "test-1767557327567-Note 1 test-1767557327567-Content 1 il y a moins dune minute" [ref=e183]:
- generic [ref=e184]:
- heading "test-1767557327567-Note 1" [level=3] [ref=e185]
- paragraph [ref=e186]: test-1767557327567-Content 1
- generic [ref=e187]: il y a moins dune minute
- generic [ref=e188]:
- button "Pin" [ref=e189]:
- img
- button "Change color" [ref=e190]:
- img
- button [ref=e191]:
- img
- button "test-1767557324248-Note 4 test-1767557324248-Content 4 il y a moins dune minute" [ref=e192]:
- generic [ref=e193]:
- heading "test-1767557324248-Note 4" [level=3] [ref=e194]
- paragraph [ref=e195]: test-1767557324248-Content 4
- generic [ref=e196]: il y a moins dune minute
- generic [ref=e197]:
- button "Pin" [ref=e198]:
- img
- button "Change color" [ref=e199]:
- img
- button [ref=e200]:
- img
- button "test-1767557324248-Note 3 test-1767557324248-Content 3 il y a moins dune minute" [ref=e201]:
- generic [ref=e202]:
- heading "test-1767557324248-Note 3" [level=3] [ref=e203]
- paragraph [ref=e204]: test-1767557324248-Content 3
- generic [ref=e205]: il y a moins dune minute
- generic [ref=e206]:
- button "Pin" [ref=e207]:
- img
- button "Change color" [ref=e208]:
- img
- button [ref=e209]:
- img
- button "test-1767557324248-Note 2 test-1767557324248-Content 2 il y a moins dune minute" [ref=e210]:
- generic [ref=e211]:
- heading "test-1767557324248-Note 2" [level=3] [ref=e212]
- paragraph [ref=e213]: test-1767557324248-Content 2
- generic [ref=e214]: il y a moins dune minute
- generic [ref=e215]:
- button "Pin" [ref=e216]:
- img
- button "Change color" [ref=e217]:
- img
- button [ref=e218]:
- img
- button "test-1767557324248-Note 1 test-1767557324248-Content 1 il y a moins dune minute" [ref=e219]:
- generic [ref=e220]:
- heading "test-1767557324248-Note 1" [level=3] [ref=e221]
- paragraph [ref=e222]: test-1767557324248-Content 1
- generic [ref=e223]: il y a moins dune minute
- generic [ref=e224]:
- button "Pin" [ref=e225]:
- img
- button "Change color" [ref=e226]:
- img
- button [ref=e227]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e228]:
- generic [ref=e229]:
- heading "Test Note for Reminder" [level=3] [ref=e230]
- paragraph [ref=e231]: This note will have a reminder
- generic [ref=e232]: il y a 26 minutes
- generic [ref=e233]:
- button "Pin" [ref=e234]:
- img
- button "Change color" [ref=e235]:
- img
- button [ref=e236]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e237]:
- generic [ref=e238]:
- heading "Test Note for Reminder" [level=3] [ref=e239]
- paragraph [ref=e240]: This note will have a reminder
- generic [ref=e241]: il y a 26 minutes
- generic [ref=e242]:
- button "Pin" [ref=e243]:
- img
- button "Change color" [ref=e244]:
- img
- button [ref=e245]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e246]:
- generic [ref=e247]:
- heading "Test Note for Reminder" [level=3] [ref=e248]
- paragraph [ref=e249]: This note will have a reminder
- generic [ref=e250]: il y a 26 minutes
- generic [ref=e251]:
- button "Pin" [ref=e252]:
- img
- button "Change color" [ref=e253]:
- img
- button [ref=e254]:
- img
- button "Test note il y a 26 minutes" [ref=e255]:
- generic [ref=e256]:
- paragraph [ref=e257]: Test note
- generic [ref=e258]: il y a 26 minutes
- generic [ref=e259]:
- button "Pin" [ref=e260]:
- img
- button "Change color" [ref=e261]:
- img
- button [ref=e262]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e263]:
- generic [ref=e264]:
- heading "Test Note for Reminder" [level=3] [ref=e265]
- paragraph [ref=e266]: This note will have a reminder
- generic [ref=e267]: il y a 26 minutes
- generic [ref=e268]:
- button "Pin" [ref=e269]:
- img
- button "Change color" [ref=e270]:
- img
- button [ref=e271]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e272]:
- generic [ref=e273]:
- heading "Test Note for Reminder" [level=3] [ref=e274]
- paragraph [ref=e275]: This note will have a reminder
- generic [ref=e276]: il y a 26 minutes
- generic [ref=e277]:
- button "Pin" [ref=e278]:
- img
- button "Change color" [ref=e279]:
- img
- button [ref=e280]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e281]:
- generic [ref=e282]:
- heading "Test Note for Reminder" [level=3] [ref=e283]
- paragraph [ref=e284]: This note will have a reminder
- generic [ref=e285]: il y a 26 minutes
- generic [ref=e286]:
- button "Pin" [ref=e287]:
- img
- button "Change color" [ref=e288]:
- img
- button [ref=e289]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e290]:
- generic [ref=e291]:
- img [ref=e292]
- heading "Test Note for Reminder" [level=3] [ref=e295]
- paragraph [ref=e296]: This note will have a reminder
- generic [ref=e297]: il y a 26 minutes
- generic [ref=e298]:
- button "Pin" [ref=e299]:
- img
- button "Change color" [ref=e300]:
- img
- button [ref=e301]:
- img
- button "test sample file il y a environ 5 heures" [ref=e302]:
- generic [ref=e303]:
- heading "test" [level=3] [ref=e304]
- paragraph [ref=e306]: sample file
- generic [ref=e307]: il y a environ 5 heures
- generic [ref=e308]:
- button "Pin" [ref=e309]:
- img
- button "Change color" [ref=e310]:
- img
- button [ref=e311]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d''un framework pour l''IA - Optimisation de l''entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l''inférence en temps réel Tags: framework, mlops, gpu, ai, tech tech ai framework mlops gpu il y a environ 5 heures" [ref=e312]':
- generic [ref=e313]:
- heading "New AI Framework Released" [level=3] [ref=e314]
- paragraph [ref=e315]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d'un framework pour l'IA - Optimisation de l'entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l'inférence en temps réel Tags: framework, mlops, gpu, ai, tech"
- generic [ref=e316]:
- generic [ref=e317]: tech
- generic [ref=e318]: ai
- generic [ref=e319]: framework
- generic [ref=e320]: mlops
- generic [ref=e321]: gpu
- generic [ref=e322]: il y a environ 5 heures
- generic [ref=e323]:
- button "Pin" [ref=e324]:
- img
- button "Change color" [ref=e325]:
- img
- button [ref=e326]:
- img
- button "Test Image API Note avec image il y a environ 8 heures" [ref=e327]:
- generic [ref=e328]:
- heading "Test Image API" [level=3] [ref=e329]
- paragraph [ref=e331]: Note avec image
- generic [ref=e332]: il y a environ 8 heures
- generic [ref=e333]:
- button "Pin" [ref=e334]:
- img
- button "Change color" [ref=e335]:
- img
- button [ref=e336]:
- img
- button "Test Markdown Titre Modifié Sous-titre édité Liste modifiée 1 Liste modifiée 2 Nouvelle liste 3 Texte gras modifié et italique édité console.log(\"Code modifié avec succès!\") il y a environ 5 heures" [ref=e337]:
- generic [ref=e338]:
- heading "Test Markdown" [level=3] [ref=e339]
- generic [ref=e341]:
- heading "Titre Modifié" [level=1] [ref=e342]
- heading "Sous-titre édité" [level=2] [ref=e343]
- list [ref=e344]:
- listitem [ref=e345]: Liste modifiée 1
- listitem [ref=e346]: Liste modifiée 2
- listitem [ref=e347]: Nouvelle liste 3
- paragraph [ref=e348]:
- strong [ref=e349]: Texte gras modifié
- text: et
- emphasis [ref=e350]: italique édité
- code [ref=e352]: console.log("Code modifié avec succès!")
- generic [ref=e353]: il y a environ 5 heures
- generic [ref=e354]:
- button "Pin" [ref=e355]:
- img
- button "Change color" [ref=e356]:
- img
- button [ref=e357]:
- img
- button "Test Image Avec image il y a environ 8 heures" [ref=e358]:
- generic [ref=e359]:
- heading "Test Image" [level=3] [ref=e360]
- paragraph [ref=e361]: Avec image
- generic [ref=e362]: il y a environ 8 heures
- generic [ref=e363]:
- button "Pin" [ref=e364]:
- img
- button "Change color" [ref=e365]:
- img
- button [ref=e366]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu tech ai framework mlops gpu il y a environ 6 heures" [ref=e367]':
- generic [ref=e368]:
- heading "New AI Framework Released" [level=3] [ref=e369]
- paragraph [ref=e370]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu"
- generic [ref=e371]:
- generic [ref=e372]: tech
- generic [ref=e373]: ai
- generic [ref=e374]: framework
- generic [ref=e375]: mlops
- generic [ref=e376]: gpu
- generic [ref=e377]: il y a environ 6 heures
- generic [ref=e378]:
- button "Pin" [ref=e379]:
- img
- button "Change color" [ref=e380]:
- img
- button [ref=e381]:
- img
- button "Test Note This is my first note to test the Google Keep clone! il y a environ 10 heures" [ref=e382]:
- generic [ref=e383]:
- img [ref=e384]
- heading "Test Note" [level=3] [ref=e387]
- paragraph [ref=e388]: This is my first note to test the Google Keep clone!
- generic [ref=e389]: il y a environ 10 heures
- generic [ref=e390]:
- button "Pin" [ref=e391]:
- img
- button "Change color" [ref=e392]:
- img
- button [ref=e393]:
- img
- button "Titre Modifié Contenu modifié avec succès! il y a environ 5 heures" [ref=e394]:
- generic [ref=e395]:
- heading "Titre Modifié" [level=3] [ref=e396]
- paragraph [ref=e397]: Contenu modifié avec succès!
- generic [ref=e398]: il y a environ 5 heures
- generic [ref=e399]:
- button "Pin" [ref=e400]:
- img
- button "Change color" [ref=e401]:
- img
- button [ref=e402]:
- img
- status [ref=e403]
- button "Open Next.js Dev Tools" [ref=e409] [cursor=pointer]:
- img [ref=e410]
- alert [ref=e413]
```

View File

@@ -1,557 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e3]:
- link "Memento" [ref=e4] [cursor=pointer]:
- /url: /
- img [ref=e5]
- generic [ref=e8]: Memento
- generic [ref=e10]:
- img [ref=e11]
- textbox "Search notes..." [ref=e14]
- button [ref=e15]:
- img
- navigation [ref=e16]:
- link "Notes" [ref=e17] [cursor=pointer]:
- /url: /
- img [ref=e18]
- text: Notes
- link "Archive" [ref=e21] [cursor=pointer]:
- /url: /archive
- img [ref=e22]
- text: Archive
- main [ref=e25]:
- generic [ref=e27]:
- textbox "Take a note..." [ref=e28]
- button "New checklist" [ref=e29]:
- img
- generic [ref=e30]:
- generic [ref=e31]:
- heading "Pinned" [level=2] [ref=e32]
- button "Updated Note avec image il y a environ 8 heures" [ref=e34]:
- generic [ref=e35]:
- img [ref=e36]
- heading "Updated" [level=3] [ref=e38]
- paragraph [ref=e39]: Note avec image
- generic [ref=e40]: il y a environ 8 heures
- generic [ref=e41]:
- button "Unpin" [ref=e42]:
- img
- button "Change color" [ref=e43]:
- img
- button [ref=e44]:
- img
- generic [ref=e45]:
- heading "Others" [level=2] [ref=e46]
- generic [ref=e47]:
- button "test-1767557370056-Note 4 test-1767557370056-Content 4 il y a moins dune minute" [ref=e48]:
- generic [ref=e49]:
- heading "test-1767557370056-Note 4" [level=3] [ref=e50]
- paragraph [ref=e51]: test-1767557370056-Content 4
- generic [ref=e52]: il y a moins dune minute
- generic [ref=e53]:
- button "Pin" [ref=e54]:
- img
- button "Change color" [ref=e55]:
- img
- button [ref=e56]:
- img
- button "test-1767557370056-Note 3 test-1767557370056-Content 3 il y a moins dune minute" [ref=e57]:
- generic [ref=e58]:
- heading "test-1767557370056-Note 3" [level=3] [ref=e59]
- paragraph [ref=e60]: test-1767557370056-Content 3
- generic [ref=e61]: il y a moins dune minute
- generic [ref=e62]:
- button "Pin" [ref=e63]:
- img
- button "Change color" [ref=e64]:
- img
- button [ref=e65]:
- img
- button "test-1767557370056-Note 2 test-1767557370056-Content 2 il y a moins dune minute" [ref=e66]:
- generic [ref=e67]:
- heading "test-1767557370056-Note 2" [level=3] [ref=e68]
- paragraph [ref=e69]: test-1767557370056-Content 2
- generic [ref=e70]: il y a moins dune minute
- generic [ref=e71]:
- button "Pin" [ref=e72]:
- img
- button "Change color" [ref=e73]:
- img
- button [ref=e74]:
- img
- button "test-1767557370056-Note 1 test-1767557370056-Content 1 il y a moins dune minute" [ref=e75]:
- generic [ref=e76]:
- heading "test-1767557370056-Note 1" [level=3] [ref=e77]
- paragraph [ref=e78]: test-1767557370056-Content 1
- generic [ref=e79]: il y a moins dune minute
- generic [ref=e80]:
- button "Pin" [ref=e81]:
- img
- button "Change color" [ref=e82]:
- img
- button [ref=e83]:
- img
- button "test-1767557339218-Note 4 test-1767557339218-Content 4 il y a 1 minute" [ref=e84]:
- generic [ref=e85]:
- heading "test-1767557339218-Note 4" [level=3] [ref=e86]
- paragraph [ref=e87]: test-1767557339218-Content 4
- generic [ref=e88]: il y a 1 minute
- generic [ref=e89]:
- button "Pin" [ref=e90]:
- img
- button "Change color" [ref=e91]:
- img
- button [ref=e92]:
- img
- button "test-1767557339218-Note 3 test-1767557339218-Content 3 il y a 1 minute" [ref=e93]:
- generic [ref=e94]:
- heading "test-1767557339218-Note 3" [level=3] [ref=e95]
- paragraph [ref=e96]: test-1767557339218-Content 3
- generic [ref=e97]: il y a 1 minute
- generic [ref=e98]:
- button "Pin" [ref=e99]:
- img
- button "Change color" [ref=e100]:
- img
- button [ref=e101]:
- img
- button "test-1767557339218-Note 2 test-1767557339218-Content 2 il y a 1 minute" [ref=e102]:
- generic [ref=e103]:
- heading "test-1767557339218-Note 2" [level=3] [ref=e104]
- paragraph [ref=e105]: test-1767557339218-Content 2
- generic [ref=e106]: il y a 1 minute
- generic [ref=e107]:
- button "Pin" [ref=e108]:
- img
- button "Change color" [ref=e109]:
- img
- button [ref=e110]:
- img
- button "test-1767557339218-Note 1 test-1767557339218-Content 1 il y a 1 minute" [ref=e111]:
- generic [ref=e112]:
- heading "test-1767557339218-Note 1" [level=3] [ref=e113]
- paragraph [ref=e114]: test-1767557339218-Content 1
- generic [ref=e115]: il y a 1 minute
- generic [ref=e116]:
- button "Pin" [ref=e117]:
- img
- button "Change color" [ref=e118]:
- img
- button [ref=e119]:
- img
- button "test-1767557334587-Note 4 test-1767557334587-Content 4 il y a 1 minute" [ref=e120]:
- generic [ref=e121]:
- heading "test-1767557334587-Note 4" [level=3] [ref=e122]
- paragraph [ref=e123]: test-1767557334587-Content 4
- generic [ref=e124]: il y a 1 minute
- generic [ref=e125]:
- button "Pin" [ref=e126]:
- img
- button "Change color" [ref=e127]:
- img
- button [ref=e128]:
- img
- button "test-1767557334587-Note 3 test-1767557334587-Content 3 il y a 1 minute" [ref=e129]:
- generic [ref=e130]:
- heading "test-1767557334587-Note 3" [level=3] [ref=e131]
- paragraph [ref=e132]: test-1767557334587-Content 3
- generic [ref=e133]: il y a 1 minute
- generic [ref=e134]:
- button "Pin" [ref=e135]:
- img
- button "Change color" [ref=e136]:
- img
- button [ref=e137]:
- img
- button "test-1767557334587-Note 2 test-1767557334587-Content 2 il y a 1 minute" [ref=e138]:
- generic [ref=e139]:
- heading "test-1767557334587-Note 2" [level=3] [ref=e140]
- paragraph [ref=e141]: test-1767557334587-Content 2
- generic [ref=e142]: il y a 1 minute
- generic [ref=e143]:
- button "Pin" [ref=e144]:
- img
- button "Change color" [ref=e145]:
- img
- button [ref=e146]:
- img
- button "test-1767557334587-Note 1 test-1767557334587-Content 1 il y a 1 minute" [ref=e147]:
- generic [ref=e148]:
- heading "test-1767557334587-Note 1" [level=3] [ref=e149]
- paragraph [ref=e150]: test-1767557334587-Content 1
- generic [ref=e151]: il y a 1 minute
- generic [ref=e152]:
- button "Pin" [ref=e153]:
- img
- button "Change color" [ref=e154]:
- img
- button [ref=e155]:
- img
- button "test-1767557330820-Note 4 test-1767557330820-Content 4 il y a 1 minute" [ref=e156]:
- generic [ref=e157]:
- heading "test-1767557330820-Note 4" [level=3] [ref=e158]
- paragraph [ref=e159]: test-1767557330820-Content 4
- generic [ref=e160]: il y a 1 minute
- generic [ref=e161]:
- button "Pin" [ref=e162]:
- img
- button "Change color" [ref=e163]:
- img
- button [ref=e164]:
- img
- button "test-1767557330820-Note 3 test-1767557330820-Content 3 il y a 1 minute" [ref=e165]:
- generic [ref=e166]:
- heading "test-1767557330820-Note 3" [level=3] [ref=e167]
- paragraph [ref=e168]: test-1767557330820-Content 3
- generic [ref=e169]: il y a 1 minute
- generic [ref=e170]:
- button "Pin" [ref=e171]:
- img
- button "Change color" [ref=e172]:
- img
- button [ref=e173]:
- img
- button "test-1767557330820-Note 2 test-1767557330820-Content 2 il y a 1 minute" [ref=e174]:
- generic [ref=e175]:
- heading "test-1767557330820-Note 2" [level=3] [ref=e176]
- paragraph [ref=e177]: test-1767557330820-Content 2
- generic [ref=e178]: il y a 1 minute
- generic [ref=e179]:
- button "Pin" [ref=e180]:
- img
- button "Change color" [ref=e181]:
- img
- button [ref=e182]:
- img
- button "test-1767557330820-Note 1 test-1767557330820-Content 1 il y a 1 minute" [ref=e183]:
- generic [ref=e184]:
- heading "test-1767557330820-Note 1" [level=3] [ref=e185]
- paragraph [ref=e186]: test-1767557330820-Content 1
- generic [ref=e187]: il y a 1 minute
- generic [ref=e188]:
- button "Pin" [ref=e189]:
- img
- button "Change color" [ref=e190]:
- img
- button [ref=e191]:
- img
- button "test-1767557327567-Note 4 test-1767557327567-Content 4 il y a 1 minute" [ref=e192]:
- generic [ref=e193]:
- heading "test-1767557327567-Note 4" [level=3] [ref=e194]
- paragraph [ref=e195]: test-1767557327567-Content 4
- generic [ref=e196]: il y a 1 minute
- generic [ref=e197]:
- button "Pin" [ref=e198]:
- img
- button "Change color" [ref=e199]:
- img
- button [ref=e200]:
- img
- button "test-1767557327567-Note 3 test-1767557327567-Content 3 il y a 1 minute" [ref=e201]:
- generic [ref=e202]:
- heading "test-1767557327567-Note 3" [level=3] [ref=e203]
- paragraph [ref=e204]: test-1767557327567-Content 3
- generic [ref=e205]: il y a 1 minute
- generic [ref=e206]:
- button "Pin" [ref=e207]:
- img
- button "Change color" [ref=e208]:
- img
- button [ref=e209]:
- img
- button "test-1767557327567-Note 2 test-1767557327567-Content 2 il y a 1 minute" [ref=e210]:
- generic [ref=e211]:
- heading "test-1767557327567-Note 2" [level=3] [ref=e212]
- paragraph [ref=e213]: test-1767557327567-Content 2
- generic [ref=e214]: il y a 1 minute
- generic [ref=e215]:
- button "Pin" [ref=e216]:
- img
- button "Change color" [ref=e217]:
- img
- button [ref=e218]:
- img
- button "test-1767557327567-Note 1 test-1767557327567-Content 1 il y a 1 minute" [ref=e219]:
- generic [ref=e220]:
- heading "test-1767557327567-Note 1" [level=3] [ref=e221]
- paragraph [ref=e222]: test-1767557327567-Content 1
- generic [ref=e223]: il y a 1 minute
- generic [ref=e224]:
- button "Pin" [ref=e225]:
- img
- button "Change color" [ref=e226]:
- img
- button [ref=e227]:
- img
- button "test-1767557324248-Note 4 test-1767557324248-Content 4 il y a 1 minute" [ref=e228]:
- generic [ref=e229]:
- heading "test-1767557324248-Note 4" [level=3] [ref=e230]
- paragraph [ref=e231]: test-1767557324248-Content 4
- generic [ref=e232]: il y a 1 minute
- generic [ref=e233]:
- button "Pin" [ref=e234]:
- img
- button "Change color" [ref=e235]:
- img
- button [ref=e236]:
- img
- button "test-1767557324248-Note 3 test-1767557324248-Content 3 il y a 1 minute" [ref=e237]:
- generic [ref=e238]:
- heading "test-1767557324248-Note 3" [level=3] [ref=e239]
- paragraph [ref=e240]: test-1767557324248-Content 3
- generic [ref=e241]: il y a 1 minute
- generic [ref=e242]:
- button "Pin" [ref=e243]:
- img
- button "Change color" [ref=e244]:
- img
- button [ref=e245]:
- img
- button "test-1767557324248-Note 2 test-1767557324248-Content 2 il y a 1 minute" [ref=e246]:
- generic [ref=e247]:
- heading "test-1767557324248-Note 2" [level=3] [ref=e248]
- paragraph [ref=e249]: test-1767557324248-Content 2
- generic [ref=e250]: il y a 1 minute
- generic [ref=e251]:
- button "Pin" [ref=e252]:
- img
- button "Change color" [ref=e253]:
- img
- button [ref=e254]:
- img
- button "test-1767557324248-Note 1 test-1767557324248-Content 1 il y a 1 minute" [ref=e255]:
- generic [ref=e256]:
- heading "test-1767557324248-Note 1" [level=3] [ref=e257]
- paragraph [ref=e258]: test-1767557324248-Content 1
- generic [ref=e259]: il y a 1 minute
- generic [ref=e260]:
- button "Pin" [ref=e261]:
- img
- button "Change color" [ref=e262]:
- img
- button [ref=e263]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e264]:
- generic [ref=e265]:
- heading "Test Note for Reminder" [level=3] [ref=e266]
- paragraph [ref=e267]: This note will have a reminder
- generic [ref=e268]: il y a 27 minutes
- generic [ref=e269]:
- button "Pin" [ref=e270]:
- img
- button "Change color" [ref=e271]:
- img
- button [ref=e272]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e273]:
- generic [ref=e274]:
- heading "Test Note for Reminder" [level=3] [ref=e275]
- paragraph [ref=e276]: This note will have a reminder
- generic [ref=e277]: il y a 27 minutes
- generic [ref=e278]:
- button "Pin" [ref=e279]:
- img
- button "Change color" [ref=e280]:
- img
- button [ref=e281]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e282]:
- generic [ref=e283]:
- heading "Test Note for Reminder" [level=3] [ref=e284]
- paragraph [ref=e285]: This note will have a reminder
- generic [ref=e286]: il y a 27 minutes
- generic [ref=e287]:
- button "Pin" [ref=e288]:
- img
- button "Change color" [ref=e289]:
- img
- button [ref=e290]:
- img
- button "Test note il y a 26 minutes" [ref=e291]:
- generic [ref=e292]:
- paragraph [ref=e293]: Test note
- generic [ref=e294]: il y a 26 minutes
- generic [ref=e295]:
- button "Pin" [ref=e296]:
- img
- button "Change color" [ref=e297]:
- img
- button [ref=e298]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e299]:
- generic [ref=e300]:
- heading "Test Note for Reminder" [level=3] [ref=e301]
- paragraph [ref=e302]: This note will have a reminder
- generic [ref=e303]: il y a 27 minutes
- generic [ref=e304]:
- button "Pin" [ref=e305]:
- img
- button "Change color" [ref=e306]:
- img
- button [ref=e307]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e308]:
- generic [ref=e309]:
- heading "Test Note for Reminder" [level=3] [ref=e310]
- paragraph [ref=e311]: This note will have a reminder
- generic [ref=e312]: il y a 27 minutes
- generic [ref=e313]:
- button "Pin" [ref=e314]:
- img
- button "Change color" [ref=e315]:
- img
- button [ref=e316]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e317]:
- generic [ref=e318]:
- heading "Test Note for Reminder" [level=3] [ref=e319]
- paragraph [ref=e320]: This note will have a reminder
- generic [ref=e321]: il y a 27 minutes
- generic [ref=e322]:
- button "Pin" [ref=e323]:
- img
- button "Change color" [ref=e324]:
- img
- button [ref=e325]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e326]:
- generic [ref=e327]:
- img [ref=e328]
- heading "Test Note for Reminder" [level=3] [ref=e331]
- paragraph [ref=e332]: This note will have a reminder
- generic [ref=e333]: il y a 27 minutes
- generic [ref=e334]:
- button "Pin" [ref=e335]:
- img
- button "Change color" [ref=e336]:
- img
- button [ref=e337]:
- img
- button "test sample file il y a environ 5 heures" [ref=e338]:
- generic [ref=e339]:
- heading "test" [level=3] [ref=e340]
- paragraph [ref=e342]: sample file
- generic [ref=e343]: il y a environ 5 heures
- generic [ref=e344]:
- button "Pin" [ref=e345]:
- img
- button "Change color" [ref=e346]:
- img
- button [ref=e347]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d''un framework pour l''IA - Optimisation de l''entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l''inférence en temps réel Tags: framework, mlops, gpu, ai, tech tech ai framework mlops gpu il y a environ 5 heures" [ref=e348]':
- generic [ref=e349]:
- heading "New AI Framework Released" [level=3] [ref=e350]
- paragraph [ref=e351]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d'un framework pour l'IA - Optimisation de l'entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l'inférence en temps réel Tags: framework, mlops, gpu, ai, tech"
- generic [ref=e352]:
- generic [ref=e353]: tech
- generic [ref=e354]: ai
- generic [ref=e355]: framework
- generic [ref=e356]: mlops
- generic [ref=e357]: gpu
- generic [ref=e358]: il y a environ 5 heures
- generic [ref=e359]:
- button "Pin" [ref=e360]:
- img
- button "Change color" [ref=e361]:
- img
- button [ref=e362]:
- img
- button "Test Image API Note avec image il y a environ 8 heures" [ref=e363]:
- generic [ref=e364]:
- heading "Test Image API" [level=3] [ref=e365]
- paragraph [ref=e367]: Note avec image
- generic [ref=e368]: il y a environ 8 heures
- generic [ref=e369]:
- button "Pin" [ref=e370]:
- img
- button "Change color" [ref=e371]:
- img
- button [ref=e372]:
- img
- button "Test Markdown Titre Modifié Sous-titre édité Liste modifiée 1 Liste modifiée 2 Nouvelle liste 3 Texte gras modifié et italique édité console.log(\"Code modifié avec succès!\") il y a environ 5 heures" [ref=e373]:
- generic [ref=e374]:
- heading "Test Markdown" [level=3] [ref=e375]
- generic [ref=e377]:
- heading "Titre Modifié" [level=1] [ref=e378]
- heading "Sous-titre édité" [level=2] [ref=e379]
- list [ref=e380]:
- listitem [ref=e381]: Liste modifiée 1
- listitem [ref=e382]: Liste modifiée 2
- listitem [ref=e383]: Nouvelle liste 3
- paragraph [ref=e384]:
- strong [ref=e385]: Texte gras modifié
- text: et
- emphasis [ref=e386]: italique édité
- code [ref=e388]: console.log("Code modifié avec succès!")
- generic [ref=e389]: il y a environ 5 heures
- generic [ref=e390]:
- button "Pin" [ref=e391]:
- img
- button "Change color" [ref=e392]:
- img
- button [ref=e393]:
- img
- button "Test Image Avec image il y a environ 8 heures" [ref=e394]:
- generic [ref=e395]:
- heading "Test Image" [level=3] [ref=e396]
- paragraph [ref=e397]: Avec image
- generic [ref=e398]: il y a environ 8 heures
- generic [ref=e399]:
- button "Pin" [ref=e400]:
- img
- button "Change color" [ref=e401]:
- img
- button [ref=e402]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu tech ai framework mlops gpu il y a environ 6 heures" [ref=e403]':
- generic [ref=e404]:
- heading "New AI Framework Released" [level=3] [ref=e405]
- paragraph [ref=e406]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu"
- generic [ref=e407]:
- generic [ref=e408]: tech
- generic [ref=e409]: ai
- generic [ref=e410]: framework
- generic [ref=e411]: mlops
- generic [ref=e412]: gpu
- generic [ref=e413]: il y a environ 6 heures
- generic [ref=e414]:
- button "Pin" [ref=e415]:
- img
- button "Change color" [ref=e416]:
- img
- button [ref=e417]:
- img
- button "Test Note This is my first note to test the Google Keep clone! il y a environ 10 heures" [ref=e418]:
- generic [ref=e419]:
- img [ref=e420]
- heading "Test Note" [level=3] [ref=e423]
- paragraph [ref=e424]: This is my first note to test the Google Keep clone!
- generic [ref=e425]: il y a environ 10 heures
- generic [ref=e426]:
- button "Pin" [ref=e427]:
- img
- button "Change color" [ref=e428]:
- img
- button [ref=e429]:
- img
- button "Titre Modifié Contenu modifié avec succès! il y a environ 5 heures" [ref=e430]:
- generic [ref=e431]:
- heading "Titre Modifié" [level=3] [ref=e432]
- paragraph [ref=e433]: Contenu modifié avec succès!
- generic [ref=e434]: il y a environ 5 heures
- generic [ref=e435]:
- button "Pin" [ref=e436]:
- img
- button "Change color" [ref=e437]:
- img
- button [ref=e438]:
- img
- status [ref=e439]
- button "Open Next.js Dev Tools" [ref=e445] [cursor=pointer]:
- img [ref=e446]
- alert [ref=e449]
```

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
// Note: datasource.url removed because we use adapter in lib/prisma.ts
});

Binary file not shown.

View File

@@ -0,0 +1,35 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"images" TEXT,
"reminder" DATETIME,
"isReminderDone" BOOLEAN NOT NULL DEFAULT false,
"reminderRecurrence" TEXT,
"reminderLocation" TEXT,
"isMarkdown" BOOLEAN NOT NULL DEFAULT false,
"userId" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isMarkdown", "isPinned", "labels", "order", "reminder", "reminderLocation", "reminderRecurrence", "title", "type", "updatedAt", "userId") SELECT "checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isMarkdown", "isPinned", "labels", "order", "reminder", "reminderLocation", "reminderRecurrence", "title", "type", "updatedAt", "userId" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
CREATE INDEX "Note_order_idx" ON "Note"("order");
CREATE INDEX "Note_reminder_idx" ON "Note"("reminder");
CREATE INDEX "Note_userId_idx" ON "Note"("userId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "links" TEXT;

View File

@@ -0,0 +1,14 @@
/*
Warnings:
- A unique constraint covering the columns `[name,userId]` on the table `Label` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "Label_name_key";
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Label_name_userId_key" ON "Label"("name", "userId");

View File

@@ -15,6 +15,7 @@ model User {
name String?
email String @unique
emailVerified DateTime?
password String? // Hashed password
image String?
accounts Account[]
sessions Session[]
@@ -65,13 +66,14 @@ model VerificationToken {
model Label {
id String @id @default(cuid())
name String @unique
name String
color String @default("gray")
userId String?
userId String? // Made optional for migration, but logic will enforce it
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([name, userId]) // Labels are unique per user
@@index([userId])
}
@@ -86,11 +88,13 @@ model Note {
checkItems String? // For checklist items stored as JSON string
labels String? // Array of label names stored as JSON string
images String? // Array of image URLs stored as JSON string
links String? // Array of link metadata stored as JSON string
reminder DateTime? // Reminder date and time
isReminderDone Boolean @default(false)
reminderRecurrence String? // "none", "daily", "weekly", "monthly", "custom"
reminderLocation String? // Location for location-based reminders
isMarkdown Boolean @default(false) // Whether content uses Markdown
userId String? // Owner of the note (optional for now, will be required after auth)
userId String? // Owner of the note
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
order Int @default(0)
createdAt DateTime @default(now())
@@ -101,4 +105,4 @@ model Note {
@@index([order])
@@index([reminder])
@@index([userId])
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

View File

@@ -0,0 +1,11 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const users = await prisma.user.findMany({
select: { email: true, name: true }
});
console.log('Registered users:', JSON.stringify(users, null, 2));
}
main().catch(console.error).finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,12 @@
import 'dotenv/config'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const users = await prisma.user.findMany()
console.log(JSON.stringify(users, null, 2))
}
main()
.catch(e => console.error(e))
.finally(async () => await prisma.$disconnect())

View File

@@ -0,0 +1,22 @@
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
const hashedPassword = await bcrypt.hash('password123', 10)
const user = await prisma.user.upsert({
where: { email: 'test@example.com' },
update: {},
create: {
email: 'test@example.com',
name: 'Test User',
password: hashedPassword,
},
})
console.log('User created:', user)
}
main()
.catch(e => console.error(e))
.finally(async () => await prisma.$disconnect())

View File

@@ -1,8 +1,4 @@
{
"status": "failed",
"failedTests": [
"1e19528dd527cbd19c0c-4c49b0bcb3667bb1a228",
"1e19528dd527cbd19c0c-039ef3f0ab8fb4aa0094",
"1e19528dd527cbd19c0c-ff6161ab584bdf7fa93d"
]
"failedTests": []
}

View File

@@ -1,557 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e3]:
- link "Memento" [ref=e4] [cursor=pointer]:
- /url: /
- img [ref=e5]
- generic [ref=e8]: Memento
- generic [ref=e10]:
- img [ref=e11]
- textbox "Search notes..." [ref=e14]
- button [ref=e15]:
- img
- navigation [ref=e16]:
- link "Notes" [ref=e17] [cursor=pointer]:
- /url: /
- img [ref=e18]
- text: Notes
- link "Archive" [ref=e21] [cursor=pointer]:
- /url: /archive
- img [ref=e22]
- text: Archive
- main [ref=e25]:
- generic [ref=e27]:
- textbox "Take a note..." [ref=e28]
- button "New checklist" [ref=e29]:
- img
- generic [ref=e30]:
- generic [ref=e31]:
- heading "Pinned" [level=2] [ref=e32]
- button "Updated Note avec image il y a environ 8 heures" [ref=e34]:
- generic [ref=e35]:
- img [ref=e36]
- heading "Updated" [level=3] [ref=e38]
- paragraph [ref=e39]: Note avec image
- generic [ref=e40]: il y a environ 8 heures
- generic [ref=e41]:
- button "Unpin" [ref=e42]:
- img
- button "Change color" [ref=e43]:
- img
- button [ref=e44]:
- img
- generic [ref=e45]:
- heading "Others" [level=2] [ref=e46]
- generic [ref=e47]:
- button "test-1767557370056-Note 4 test-1767557370056-Content 4 il y a moins dune minute" [ref=e48]:
- generic [ref=e49]:
- heading "test-1767557370056-Note 4" [level=3] [ref=e50]
- paragraph [ref=e51]: test-1767557370056-Content 4
- generic [ref=e52]: il y a moins dune minute
- generic [ref=e53]:
- button "Pin" [ref=e54]:
- img
- button "Change color" [ref=e55]:
- img
- button [ref=e56]:
- img
- button "test-1767557370056-Note 3 test-1767557370056-Content 3 il y a moins dune minute" [ref=e57]:
- generic [ref=e58]:
- heading "test-1767557370056-Note 3" [level=3] [ref=e59]
- paragraph [ref=e60]: test-1767557370056-Content 3
- generic [ref=e61]: il y a moins dune minute
- generic [ref=e62]:
- button "Pin" [ref=e63]:
- img
- button "Change color" [ref=e64]:
- img
- button [ref=e65]:
- img
- button "test-1767557370056-Note 2 test-1767557370056-Content 2 il y a moins dune minute" [ref=e66]:
- generic [ref=e67]:
- heading "test-1767557370056-Note 2" [level=3] [ref=e68]
- paragraph [ref=e69]: test-1767557370056-Content 2
- generic [ref=e70]: il y a moins dune minute
- generic [ref=e71]:
- button "Pin" [ref=e72]:
- img
- button "Change color" [ref=e73]:
- img
- button [ref=e74]:
- img
- button "test-1767557370056-Note 1 test-1767557370056-Content 1 il y a moins dune minute" [ref=e75]:
- generic [ref=e76]:
- heading "test-1767557370056-Note 1" [level=3] [ref=e77]
- paragraph [ref=e78]: test-1767557370056-Content 1
- generic [ref=e79]: il y a moins dune minute
- generic [ref=e80]:
- button "Pin" [ref=e81]:
- img
- button "Change color" [ref=e82]:
- img
- button [ref=e83]:
- img
- button "test-1767557339218-Note 4 test-1767557339218-Content 4 il y a 1 minute" [ref=e84]:
- generic [ref=e85]:
- heading "test-1767557339218-Note 4" [level=3] [ref=e86]
- paragraph [ref=e87]: test-1767557339218-Content 4
- generic [ref=e88]: il y a 1 minute
- generic [ref=e89]:
- button "Pin" [ref=e90]:
- img
- button "Change color" [ref=e91]:
- img
- button [ref=e92]:
- img
- button "test-1767557339218-Note 3 test-1767557339218-Content 3 il y a 1 minute" [ref=e93]:
- generic [ref=e94]:
- heading "test-1767557339218-Note 3" [level=3] [ref=e95]
- paragraph [ref=e96]: test-1767557339218-Content 3
- generic [ref=e97]: il y a 1 minute
- generic [ref=e98]:
- button "Pin" [ref=e99]:
- img
- button "Change color" [ref=e100]:
- img
- button [ref=e101]:
- img
- button "test-1767557339218-Note 2 test-1767557339218-Content 2 il y a 1 minute" [ref=e102]:
- generic [ref=e103]:
- heading "test-1767557339218-Note 2" [level=3] [ref=e104]
- paragraph [ref=e105]: test-1767557339218-Content 2
- generic [ref=e106]: il y a 1 minute
- generic [ref=e107]:
- button "Pin" [ref=e108]:
- img
- button "Change color" [ref=e109]:
- img
- button [ref=e110]:
- img
- button "test-1767557339218-Note 1 test-1767557339218-Content 1 il y a 1 minute" [ref=e111]:
- generic [ref=e112]:
- heading "test-1767557339218-Note 1" [level=3] [ref=e113]
- paragraph [ref=e114]: test-1767557339218-Content 1
- generic [ref=e115]: il y a 1 minute
- generic [ref=e116]:
- button "Pin" [ref=e117]:
- img
- button "Change color" [ref=e118]:
- img
- button [ref=e119]:
- img
- button "test-1767557334587-Note 4 test-1767557334587-Content 4 il y a 1 minute" [ref=e120]:
- generic [ref=e121]:
- heading "test-1767557334587-Note 4" [level=3] [ref=e122]
- paragraph [ref=e123]: test-1767557334587-Content 4
- generic [ref=e124]: il y a 1 minute
- generic [ref=e125]:
- button "Pin" [ref=e126]:
- img
- button "Change color" [ref=e127]:
- img
- button [ref=e128]:
- img
- button "test-1767557334587-Note 3 test-1767557334587-Content 3 il y a 1 minute" [ref=e129]:
- generic [ref=e130]:
- heading "test-1767557334587-Note 3" [level=3] [ref=e131]
- paragraph [ref=e132]: test-1767557334587-Content 3
- generic [ref=e133]: il y a 1 minute
- generic [ref=e134]:
- button "Pin" [ref=e135]:
- img
- button "Change color" [ref=e136]:
- img
- button [ref=e137]:
- img
- button "test-1767557334587-Note 2 test-1767557334587-Content 2 il y a 1 minute" [ref=e138]:
- generic [ref=e139]:
- heading "test-1767557334587-Note 2" [level=3] [ref=e140]
- paragraph [ref=e141]: test-1767557334587-Content 2
- generic [ref=e142]: il y a 1 minute
- generic [ref=e143]:
- button "Pin" [ref=e144]:
- img
- button "Change color" [ref=e145]:
- img
- button [ref=e146]:
- img
- button "test-1767557334587-Note 1 test-1767557334587-Content 1 il y a 1 minute" [ref=e147]:
- generic [ref=e148]:
- heading "test-1767557334587-Note 1" [level=3] [ref=e149]
- paragraph [ref=e150]: test-1767557334587-Content 1
- generic [ref=e151]: il y a 1 minute
- generic [ref=e152]:
- button "Pin" [ref=e153]:
- img
- button "Change color" [ref=e154]:
- img
- button [ref=e155]:
- img
- button "test-1767557330820-Note 4 test-1767557330820-Content 4 il y a 1 minute" [ref=e156]:
- generic [ref=e157]:
- heading "test-1767557330820-Note 4" [level=3] [ref=e158]
- paragraph [ref=e159]: test-1767557330820-Content 4
- generic [ref=e160]: il y a 1 minute
- generic [ref=e161]:
- button "Pin" [ref=e162]:
- img
- button "Change color" [ref=e163]:
- img
- button [ref=e164]:
- img
- button "test-1767557330820-Note 3 test-1767557330820-Content 3 il y a 1 minute" [ref=e165]:
- generic [ref=e166]:
- heading "test-1767557330820-Note 3" [level=3] [ref=e167]
- paragraph [ref=e168]: test-1767557330820-Content 3
- generic [ref=e169]: il y a 1 minute
- generic [ref=e170]:
- button "Pin" [ref=e171]:
- img
- button "Change color" [ref=e172]:
- img
- button [ref=e173]:
- img
- button "test-1767557330820-Note 2 test-1767557330820-Content 2 il y a 1 minute" [ref=e174]:
- generic [ref=e175]:
- heading "test-1767557330820-Note 2" [level=3] [ref=e176]
- paragraph [ref=e177]: test-1767557330820-Content 2
- generic [ref=e178]: il y a 1 minute
- generic [ref=e179]:
- button "Pin" [ref=e180]:
- img
- button "Change color" [ref=e181]:
- img
- button [ref=e182]:
- img
- button "test-1767557330820-Note 1 test-1767557330820-Content 1 il y a 1 minute" [ref=e183]:
- generic [ref=e184]:
- heading "test-1767557330820-Note 1" [level=3] [ref=e185]
- paragraph [ref=e186]: test-1767557330820-Content 1
- generic [ref=e187]: il y a 1 minute
- generic [ref=e188]:
- button "Pin" [ref=e189]:
- img
- button "Change color" [ref=e190]:
- img
- button [ref=e191]:
- img
- button "test-1767557327567-Note 4 test-1767557327567-Content 4 il y a 1 minute" [ref=e192]:
- generic [ref=e193]:
- heading "test-1767557327567-Note 4" [level=3] [ref=e194]
- paragraph [ref=e195]: test-1767557327567-Content 4
- generic [ref=e196]: il y a 1 minute
- generic [ref=e197]:
- button "Pin" [ref=e198]:
- img
- button "Change color" [ref=e199]:
- img
- button [ref=e200]:
- img
- button "test-1767557327567-Note 3 test-1767557327567-Content 3 il y a 1 minute" [ref=e201]:
- generic [ref=e202]:
- heading "test-1767557327567-Note 3" [level=3] [ref=e203]
- paragraph [ref=e204]: test-1767557327567-Content 3
- generic [ref=e205]: il y a 1 minute
- generic [ref=e206]:
- button "Pin" [ref=e207]:
- img
- button "Change color" [ref=e208]:
- img
- button [ref=e209]:
- img
- button "test-1767557327567-Note 2 test-1767557327567-Content 2 il y a 1 minute" [ref=e210]:
- generic [ref=e211]:
- heading "test-1767557327567-Note 2" [level=3] [ref=e212]
- paragraph [ref=e213]: test-1767557327567-Content 2
- generic [ref=e214]: il y a 1 minute
- generic [ref=e215]:
- button "Pin" [ref=e216]:
- img
- button "Change color" [ref=e217]:
- img
- button [ref=e218]:
- img
- button "test-1767557327567-Note 1 test-1767557327567-Content 1 il y a 1 minute" [ref=e219]:
- generic [ref=e220]:
- heading "test-1767557327567-Note 1" [level=3] [ref=e221]
- paragraph [ref=e222]: test-1767557327567-Content 1
- generic [ref=e223]: il y a 1 minute
- generic [ref=e224]:
- button "Pin" [ref=e225]:
- img
- button "Change color" [ref=e226]:
- img
- button [ref=e227]:
- img
- button "test-1767557324248-Note 4 test-1767557324248-Content 4 il y a 1 minute" [ref=e228]:
- generic [ref=e229]:
- heading "test-1767557324248-Note 4" [level=3] [ref=e230]
- paragraph [ref=e231]: test-1767557324248-Content 4
- generic [ref=e232]: il y a 1 minute
- generic [ref=e233]:
- button "Pin" [ref=e234]:
- img
- button "Change color" [ref=e235]:
- img
- button [ref=e236]:
- img
- button "test-1767557324248-Note 3 test-1767557324248-Content 3 il y a 1 minute" [ref=e237]:
- generic [ref=e238]:
- heading "test-1767557324248-Note 3" [level=3] [ref=e239]
- paragraph [ref=e240]: test-1767557324248-Content 3
- generic [ref=e241]: il y a 1 minute
- generic [ref=e242]:
- button "Pin" [ref=e243]:
- img
- button "Change color" [ref=e244]:
- img
- button [ref=e245]:
- img
- button "test-1767557324248-Note 2 test-1767557324248-Content 2 il y a 1 minute" [ref=e246]:
- generic [ref=e247]:
- heading "test-1767557324248-Note 2" [level=3] [ref=e248]
- paragraph [ref=e249]: test-1767557324248-Content 2
- generic [ref=e250]: il y a 1 minute
- generic [ref=e251]:
- button "Pin" [ref=e252]:
- img
- button "Change color" [ref=e253]:
- img
- button [ref=e254]:
- img
- button "test-1767557324248-Note 1 test-1767557324248-Content 1 il y a 1 minute" [ref=e255]:
- generic [ref=e256]:
- heading "test-1767557324248-Note 1" [level=3] [ref=e257]
- paragraph [ref=e258]: test-1767557324248-Content 1
- generic [ref=e259]: il y a 1 minute
- generic [ref=e260]:
- button "Pin" [ref=e261]:
- img
- button "Change color" [ref=e262]:
- img
- button [ref=e263]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e264]:
- generic [ref=e265]:
- heading "Test Note for Reminder" [level=3] [ref=e266]
- paragraph [ref=e267]: This note will have a reminder
- generic [ref=e268]: il y a 27 minutes
- generic [ref=e269]:
- button "Pin" [ref=e270]:
- img
- button "Change color" [ref=e271]:
- img
- button [ref=e272]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e273]:
- generic [ref=e274]:
- heading "Test Note for Reminder" [level=3] [ref=e275]
- paragraph [ref=e276]: This note will have a reminder
- generic [ref=e277]: il y a 27 minutes
- generic [ref=e278]:
- button "Pin" [ref=e279]:
- img
- button "Change color" [ref=e280]:
- img
- button [ref=e281]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e282]:
- generic [ref=e283]:
- heading "Test Note for Reminder" [level=3] [ref=e284]
- paragraph [ref=e285]: This note will have a reminder
- generic [ref=e286]: il y a 27 minutes
- generic [ref=e287]:
- button "Pin" [ref=e288]:
- img
- button "Change color" [ref=e289]:
- img
- button [ref=e290]:
- img
- button "Test note il y a 26 minutes" [ref=e291]:
- generic [ref=e292]:
- paragraph [ref=e293]: Test note
- generic [ref=e294]: il y a 26 minutes
- generic [ref=e295]:
- button "Pin" [ref=e296]:
- img
- button "Change color" [ref=e297]:
- img
- button [ref=e298]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e299]:
- generic [ref=e300]:
- heading "Test Note for Reminder" [level=3] [ref=e301]
- paragraph [ref=e302]: This note will have a reminder
- generic [ref=e303]: il y a 27 minutes
- generic [ref=e304]:
- button "Pin" [ref=e305]:
- img
- button "Change color" [ref=e306]:
- img
- button [ref=e307]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e308]:
- generic [ref=e309]:
- heading "Test Note for Reminder" [level=3] [ref=e310]
- paragraph [ref=e311]: This note will have a reminder
- generic [ref=e312]: il y a 27 minutes
- generic [ref=e313]:
- button "Pin" [ref=e314]:
- img
- button "Change color" [ref=e315]:
- img
- button [ref=e316]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e317]:
- generic [ref=e318]:
- heading "Test Note for Reminder" [level=3] [ref=e319]
- paragraph [ref=e320]: This note will have a reminder
- generic [ref=e321]: il y a 27 minutes
- generic [ref=e322]:
- button "Pin" [ref=e323]:
- img
- button "Change color" [ref=e324]:
- img
- button [ref=e325]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 27 minutes" [ref=e326]:
- generic [ref=e327]:
- img [ref=e328]
- heading "Test Note for Reminder" [level=3] [ref=e331]
- paragraph [ref=e332]: This note will have a reminder
- generic [ref=e333]: il y a 27 minutes
- generic [ref=e334]:
- button "Pin" [ref=e335]:
- img
- button "Change color" [ref=e336]:
- img
- button [ref=e337]:
- img
- button "test sample file il y a environ 5 heures" [ref=e338]:
- generic [ref=e339]:
- heading "test" [level=3] [ref=e340]
- paragraph [ref=e342]: sample file
- generic [ref=e343]: il y a environ 5 heures
- generic [ref=e344]:
- button "Pin" [ref=e345]:
- img
- button "Change color" [ref=e346]:
- img
- button [ref=e347]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d''un framework pour l''IA - Optimisation de l''entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l''inférence en temps réel Tags: framework, mlops, gpu, ai, tech tech ai framework mlops gpu il y a environ 5 heures" [ref=e348]':
- generic [ref=e349]:
- heading "New AI Framework Released" [level=3] [ref=e350]
- paragraph [ref=e351]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d'un framework pour l'IA - Optimisation de l'entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l'inférence en temps réel Tags: framework, mlops, gpu, ai, tech"
- generic [ref=e352]:
- generic [ref=e353]: tech
- generic [ref=e354]: ai
- generic [ref=e355]: framework
- generic [ref=e356]: mlops
- generic [ref=e357]: gpu
- generic [ref=e358]: il y a environ 5 heures
- generic [ref=e359]:
- button "Pin" [ref=e360]:
- img
- button "Change color" [ref=e361]:
- img
- button [ref=e362]:
- img
- button "Test Image API Note avec image il y a environ 8 heures" [ref=e363]:
- generic [ref=e364]:
- heading "Test Image API" [level=3] [ref=e365]
- paragraph [ref=e367]: Note avec image
- generic [ref=e368]: il y a environ 8 heures
- generic [ref=e369]:
- button "Pin" [ref=e370]:
- img
- button "Change color" [ref=e371]:
- img
- button [ref=e372]:
- img
- button "Test Markdown Titre Modifié Sous-titre édité Liste modifiée 1 Liste modifiée 2 Nouvelle liste 3 Texte gras modifié et italique édité console.log(\"Code modifié avec succès!\") il y a environ 5 heures" [ref=e373]:
- generic [ref=e374]:
- heading "Test Markdown" [level=3] [ref=e375]
- generic [ref=e377]:
- heading "Titre Modifié" [level=1] [ref=e378]
- heading "Sous-titre édité" [level=2] [ref=e379]
- list [ref=e380]:
- listitem [ref=e381]: Liste modifiée 1
- listitem [ref=e382]: Liste modifiée 2
- listitem [ref=e383]: Nouvelle liste 3
- paragraph [ref=e384]:
- strong [ref=e385]: Texte gras modifié
- text: et
- emphasis [ref=e386]: italique édité
- code [ref=e388]: console.log("Code modifié avec succès!")
- generic [ref=e389]: il y a environ 5 heures
- generic [ref=e390]:
- button "Pin" [ref=e391]:
- img
- button "Change color" [ref=e392]:
- img
- button [ref=e393]:
- img
- button "Test Image Avec image il y a environ 8 heures" [ref=e394]:
- generic [ref=e395]:
- heading "Test Image" [level=3] [ref=e396]
- paragraph [ref=e397]: Avec image
- generic [ref=e398]: il y a environ 8 heures
- generic [ref=e399]:
- button "Pin" [ref=e400]:
- img
- button "Change color" [ref=e401]:
- img
- button [ref=e402]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu tech ai framework mlops gpu il y a environ 6 heures" [ref=e403]':
- generic [ref=e404]:
- heading "New AI Framework Released" [level=3] [ref=e405]
- paragraph [ref=e406]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu"
- generic [ref=e407]:
- generic [ref=e408]: tech
- generic [ref=e409]: ai
- generic [ref=e410]: framework
- generic [ref=e411]: mlops
- generic [ref=e412]: gpu
- generic [ref=e413]: il y a environ 6 heures
- generic [ref=e414]:
- button "Pin" [ref=e415]:
- img
- button "Change color" [ref=e416]:
- img
- button [ref=e417]:
- img
- button "Test Note This is my first note to test the Google Keep clone! il y a environ 10 heures" [ref=e418]:
- generic [ref=e419]:
- img [ref=e420]
- heading "Test Note" [level=3] [ref=e423]
- paragraph [ref=e424]: This is my first note to test the Google Keep clone!
- generic [ref=e425]: il y a environ 10 heures
- generic [ref=e426]:
- button "Pin" [ref=e427]:
- img
- button "Change color" [ref=e428]:
- img
- button [ref=e429]:
- img
- button "Titre Modifié Contenu modifié avec succès! il y a environ 5 heures" [ref=e430]:
- generic [ref=e431]:
- heading "Titre Modifié" [level=3] [ref=e432]
- paragraph [ref=e433]: Contenu modifié avec succès!
- generic [ref=e434]: il y a environ 5 heures
- generic [ref=e435]:
- button "Pin" [ref=e436]:
- img
- button "Change color" [ref=e437]:
- img
- button [ref=e438]:
- img
- status [ref=e439]
- button "Open Next.js Dev Tools" [ref=e445] [cursor=pointer]:
- img [ref=e446]
- alert [ref=e449]
```

View File

@@ -1,478 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e3]:
- link "Memento" [ref=e4] [cursor=pointer]:
- /url: /
- img [ref=e5]
- generic [ref=e8]: Memento
- generic [ref=e10]:
- img [ref=e11]
- textbox "Search notes..." [ref=e14]
- button [ref=e15]:
- img
- navigation [ref=e16]:
- link "Notes" [ref=e17] [cursor=pointer]:
- /url: /
- img [ref=e18]
- text: Notes
- link "Archive" [ref=e21] [cursor=pointer]:
- /url: /archive
- img [ref=e22]
- text: Archive
- main [ref=e25]:
- generic [ref=e27]:
- textbox "Take a note..." [ref=e28]
- button "New checklist" [ref=e29]:
- img
- generic [ref=e30]:
- generic [ref=e31]:
- heading "Pinned" [level=2] [ref=e32]
- button "Updated Note avec image il y a environ 8 heures" [ref=e34]:
- generic [ref=e35]:
- img [ref=e36]
- heading "Updated" [level=3] [ref=e38]
- paragraph [ref=e39]: Note avec image
- generic [ref=e40]: il y a environ 8 heures
- generic [ref=e41]:
- button "Unpin" [ref=e42]:
- img
- button "Change color" [ref=e43]:
- img
- button [ref=e44]:
- img
- generic [ref=e45]:
- heading "Others" [level=2] [ref=e46]
- generic [ref=e47]:
- button "test-1767557334587-Note 4 test-1767557334587-Content 4 il y a moins dune minute" [ref=e48]:
- generic [ref=e49]:
- heading "test-1767557334587-Note 4" [level=3] [ref=e50]
- paragraph [ref=e51]: test-1767557334587-Content 4
- generic [ref=e52]: il y a moins dune minute
- generic [ref=e53]:
- button "Pin" [ref=e54]:
- img
- button "Change color" [ref=e55]:
- img
- button [ref=e56]:
- img
- button "test-1767557334587-Note 3 test-1767557334587-Content 3 il y a moins dune minute" [ref=e57]:
- generic [ref=e58]:
- heading "test-1767557334587-Note 3" [level=3] [ref=e59]
- paragraph [ref=e60]: test-1767557334587-Content 3
- generic [ref=e61]: il y a moins dune minute
- generic [ref=e62]:
- button "Pin" [ref=e63]:
- img
- button "Change color" [ref=e64]:
- img
- button [ref=e65]:
- img
- button "test-1767557334587-Note 2 test-1767557334587-Content 2 il y a moins dune minute" [ref=e66]:
- generic [ref=e67]:
- heading "test-1767557334587-Note 2" [level=3] [ref=e68]
- paragraph [ref=e69]: test-1767557334587-Content 2
- generic [ref=e70]: il y a moins dune minute
- generic [ref=e71]:
- button "Pin" [ref=e72]:
- img
- button "Change color" [ref=e73]:
- img
- button [ref=e74]:
- img
- button "test-1767557334587-Note 1 test-1767557334587-Content 1 il y a moins dune minute" [ref=e75]:
- generic [ref=e76]:
- heading "test-1767557334587-Note 1" [level=3] [ref=e77]
- paragraph [ref=e78]: test-1767557334587-Content 1
- generic [ref=e79]: il y a moins dune minute
- generic [ref=e80]:
- button "Pin" [ref=e81]:
- img
- button "Change color" [ref=e82]:
- img
- button [ref=e83]:
- img
- button "test-1767557330820-Note 4 test-1767557330820-Content 4 il y a moins dune minute" [ref=e84]:
- generic [ref=e85]:
- heading "test-1767557330820-Note 4" [level=3] [ref=e86]
- paragraph [ref=e87]: test-1767557330820-Content 4
- generic [ref=e88]: il y a moins dune minute
- generic [ref=e89]:
- button "Pin" [ref=e90]:
- img
- button "Change color" [ref=e91]:
- img
- button [ref=e92]:
- img
- button "test-1767557330820-Note 3 test-1767557330820-Content 3 il y a moins dune minute" [ref=e93]:
- generic [ref=e94]:
- heading "test-1767557330820-Note 3" [level=3] [ref=e95]
- paragraph [ref=e96]: test-1767557330820-Content 3
- generic [ref=e97]: il y a moins dune minute
- generic [ref=e98]:
- button "Pin" [ref=e99]:
- img
- button "Change color" [ref=e100]:
- img
- button [ref=e101]:
- img
- button "test-1767557330820-Note 2 test-1767557330820-Content 2 il y a moins dune minute" [ref=e102]:
- generic [ref=e103]:
- heading "test-1767557330820-Note 2" [level=3] [ref=e104]
- paragraph [ref=e105]: test-1767557330820-Content 2
- generic [ref=e106]: il y a moins dune minute
- generic [ref=e107]:
- button "Pin" [ref=e108]:
- img
- button "Change color" [ref=e109]:
- img
- button [ref=e110]:
- img
- button "test-1767557330820-Note 1 test-1767557330820-Content 1 il y a moins dune minute" [ref=e111]:
- generic [ref=e112]:
- heading "test-1767557330820-Note 1" [level=3] [ref=e113]
- paragraph [ref=e114]: test-1767557330820-Content 1
- generic [ref=e115]: il y a moins dune minute
- generic [ref=e116]:
- button "Pin" [ref=e117]:
- img
- button "Change color" [ref=e118]:
- img
- button [ref=e119]:
- img
- button "test-1767557327567-Note 4 test-1767557327567-Content 4 il y a moins dune minute" [ref=e120]:
- generic [ref=e121]:
- heading "test-1767557327567-Note 4" [level=3] [ref=e122]
- paragraph [ref=e123]: test-1767557327567-Content 4
- generic [ref=e124]: il y a moins dune minute
- generic [ref=e125]:
- button "Pin" [ref=e126]:
- img
- button "Change color" [ref=e127]:
- img
- button [ref=e128]:
- img
- button "test-1767557327567-Note 3 test-1767557327567-Content 3 il y a moins dune minute" [ref=e129]:
- generic [ref=e130]:
- heading "test-1767557327567-Note 3" [level=3] [ref=e131]
- paragraph [ref=e132]: test-1767557327567-Content 3
- generic [ref=e133]: il y a moins dune minute
- generic [ref=e134]:
- button "Pin" [ref=e135]:
- img
- button "Change color" [ref=e136]:
- img
- button [ref=e137]:
- img
- button "test-1767557327567-Note 2 test-1767557327567-Content 2 il y a moins dune minute" [ref=e138]:
- generic [ref=e139]:
- heading "test-1767557327567-Note 2" [level=3] [ref=e140]
- paragraph [ref=e141]: test-1767557327567-Content 2
- generic [ref=e142]: il y a moins dune minute
- generic [ref=e143]:
- button "Pin" [ref=e144]:
- img
- button "Change color" [ref=e145]:
- img
- button [ref=e146]:
- img
- button "test-1767557327567-Note 1 test-1767557327567-Content 1 il y a moins dune minute" [ref=e147]:
- generic [ref=e148]:
- heading "test-1767557327567-Note 1" [level=3] [ref=e149]
- paragraph [ref=e150]: test-1767557327567-Content 1
- generic [ref=e151]: il y a moins dune minute
- generic [ref=e152]:
- button "Pin" [ref=e153]:
- img
- button "Change color" [ref=e154]:
- img
- button [ref=e155]:
- img
- button "test-1767557324248-Note 4 test-1767557324248-Content 4 il y a moins dune minute" [ref=e156]:
- generic [ref=e157]:
- heading "test-1767557324248-Note 4" [level=3] [ref=e158]
- paragraph [ref=e159]: test-1767557324248-Content 4
- generic [ref=e160]: il y a moins dune minute
- generic [ref=e161]:
- button "Pin" [ref=e162]:
- img
- button "Change color" [ref=e163]:
- img
- button [ref=e164]:
- img
- button "test-1767557324248-Note 3 test-1767557324248-Content 3 il y a moins dune minute" [ref=e165]:
- generic [ref=e166]:
- heading "test-1767557324248-Note 3" [level=3] [ref=e167]
- paragraph [ref=e168]: test-1767557324248-Content 3
- generic [ref=e169]: il y a moins dune minute
- generic [ref=e170]:
- button "Pin" [ref=e171]:
- img
- button "Change color" [ref=e172]:
- img
- button [ref=e173]:
- img
- button "test-1767557324248-Note 2 test-1767557324248-Content 2 il y a moins dune minute" [ref=e174]:
- generic [ref=e175]:
- heading "test-1767557324248-Note 2" [level=3] [ref=e176]
- paragraph [ref=e177]: test-1767557324248-Content 2
- generic [ref=e178]: il y a moins dune minute
- generic [ref=e179]:
- button "Pin" [ref=e180]:
- img
- button "Change color" [ref=e181]:
- img
- button [ref=e182]:
- img
- button "test-1767557324248-Note 1 test-1767557324248-Content 1 il y a moins dune minute" [ref=e183]:
- generic [ref=e184]:
- heading "test-1767557324248-Note 1" [level=3] [ref=e185]
- paragraph [ref=e186]: test-1767557324248-Content 1
- generic [ref=e187]: il y a moins dune minute
- generic [ref=e188]:
- button "Pin" [ref=e189]:
- img
- button "Change color" [ref=e190]:
- img
- button [ref=e191]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e192]:
- generic [ref=e193]:
- heading "Test Note for Reminder" [level=3] [ref=e194]
- paragraph [ref=e195]: This note will have a reminder
- generic [ref=e196]: il y a 26 minutes
- generic [ref=e197]:
- button "Pin" [ref=e198]:
- img
- button "Change color" [ref=e199]:
- img
- button [ref=e200]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e201]:
- generic [ref=e202]:
- heading "Test Note for Reminder" [level=3] [ref=e203]
- paragraph [ref=e204]: This note will have a reminder
- generic [ref=e205]: il y a 26 minutes
- generic [ref=e206]:
- button "Pin" [ref=e207]:
- img
- button "Change color" [ref=e208]:
- img
- button [ref=e209]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e210]:
- generic [ref=e211]:
- heading "Test Note for Reminder" [level=3] [ref=e212]
- paragraph [ref=e213]: This note will have a reminder
- generic [ref=e214]: il y a 26 minutes
- generic [ref=e215]:
- button "Pin" [ref=e216]:
- img
- button "Change color" [ref=e217]:
- img
- button [ref=e218]:
- img
- button "Test note il y a 26 minutes" [ref=e219]:
- generic [ref=e220]:
- paragraph [ref=e221]: Test note
- generic [ref=e222]: il y a 26 minutes
- generic [ref=e223]:
- button "Pin" [ref=e224]:
- img
- button "Change color" [ref=e225]:
- img
- button [ref=e226]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e227]:
- generic [ref=e228]:
- heading "Test Note for Reminder" [level=3] [ref=e229]
- paragraph [ref=e230]: This note will have a reminder
- generic [ref=e231]: il y a 26 minutes
- generic [ref=e232]:
- button "Pin" [ref=e233]:
- img
- button "Change color" [ref=e234]:
- img
- button [ref=e235]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e236]:
- generic [ref=e237]:
- heading "Test Note for Reminder" [level=3] [ref=e238]
- paragraph [ref=e239]: This note will have a reminder
- generic [ref=e240]: il y a 26 minutes
- generic [ref=e241]:
- button "Pin" [ref=e242]:
- img
- button "Change color" [ref=e243]:
- img
- button [ref=e244]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e245]:
- generic [ref=e246]:
- heading "Test Note for Reminder" [level=3] [ref=e247]
- paragraph [ref=e248]: This note will have a reminder
- generic [ref=e249]: il y a 26 minutes
- generic [ref=e250]:
- button "Pin" [ref=e251]:
- img
- button "Change color" [ref=e252]:
- img
- button [ref=e253]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e254]:
- generic [ref=e255]:
- img [ref=e256]
- heading "Test Note for Reminder" [level=3] [ref=e259]
- paragraph [ref=e260]: This note will have a reminder
- generic [ref=e261]: il y a 26 minutes
- generic [ref=e262]:
- button "Pin" [ref=e263]:
- img
- button "Change color" [ref=e264]:
- img
- button [ref=e265]:
- img
- button "test sample file il y a environ 5 heures" [ref=e266]:
- generic [ref=e267]:
- heading "test" [level=3] [ref=e268]
- paragraph [ref=e270]: sample file
- generic [ref=e271]: il y a environ 5 heures
- generic [ref=e272]:
- button "Pin" [ref=e273]:
- img
- button "Change color" [ref=e274]:
- img
- button [ref=e275]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d''un framework pour l''IA - Optimisation de l''entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l''inférence en temps réel Tags: framework, mlops, gpu, ai, tech tech ai framework mlops gpu il y a environ 5 heures" [ref=e276]':
- generic [ref=e277]:
- heading "New AI Framework Released" [level=3] [ref=e278]
- paragraph [ref=e279]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d'un framework pour l'IA - Optimisation de l'entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l'inférence en temps réel Tags: framework, mlops, gpu, ai, tech"
- generic [ref=e280]:
- generic [ref=e281]: tech
- generic [ref=e282]: ai
- generic [ref=e283]: framework
- generic [ref=e284]: mlops
- generic [ref=e285]: gpu
- generic [ref=e286]: il y a environ 5 heures
- generic [ref=e287]:
- button "Pin" [ref=e288]:
- img
- button "Change color" [ref=e289]:
- img
- button [ref=e290]:
- img
- button "Test Image API Note avec image il y a environ 8 heures" [ref=e291]:
- generic [ref=e292]:
- heading "Test Image API" [level=3] [ref=e293]
- paragraph [ref=e295]: Note avec image
- generic [ref=e296]: il y a environ 8 heures
- generic [ref=e297]:
- button "Pin" [ref=e298]:
- img
- button "Change color" [ref=e299]:
- img
- button [ref=e300]:
- img
- button "Test Markdown Titre Modifié Sous-titre édité Liste modifiée 1 Liste modifiée 2 Nouvelle liste 3 Texte gras modifié et italique édité console.log(\"Code modifié avec succès!\") il y a environ 5 heures" [ref=e301]:
- generic [ref=e302]:
- heading "Test Markdown" [level=3] [ref=e303]
- generic [ref=e305]:
- heading "Titre Modifié" [level=1] [ref=e306]
- heading "Sous-titre édité" [level=2] [ref=e307]
- list [ref=e308]:
- listitem [ref=e309]: Liste modifiée 1
- listitem [ref=e310]: Liste modifiée 2
- listitem [ref=e311]: Nouvelle liste 3
- paragraph [ref=e312]:
- strong [ref=e313]: Texte gras modifié
- text: et
- emphasis [ref=e314]: italique édité
- code [ref=e316]: console.log("Code modifié avec succès!")
- generic [ref=e317]: il y a environ 5 heures
- generic [ref=e318]:
- button "Pin" [ref=e319]:
- img
- button "Change color" [ref=e320]:
- img
- button [ref=e321]:
- img
- button "Test Image Avec image il y a environ 8 heures" [ref=e322]:
- generic [ref=e323]:
- heading "Test Image" [level=3] [ref=e324]
- paragraph [ref=e325]: Avec image
- generic [ref=e326]: il y a environ 8 heures
- generic [ref=e327]:
- button "Pin" [ref=e328]:
- img
- button "Change color" [ref=e329]:
- img
- button [ref=e330]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu tech ai framework mlops gpu il y a environ 6 heures" [ref=e331]':
- generic [ref=e332]:
- heading "New AI Framework Released" [level=3] [ref=e333]
- paragraph [ref=e334]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu"
- generic [ref=e335]:
- generic [ref=e336]: tech
- generic [ref=e337]: ai
- generic [ref=e338]: framework
- generic [ref=e339]: mlops
- generic [ref=e340]: gpu
- generic [ref=e341]: il y a environ 6 heures
- generic [ref=e342]:
- button "Pin" [ref=e343]:
- img
- button "Change color" [ref=e344]:
- img
- button [ref=e345]:
- img
- button "Test Note This is my first note to test the Google Keep clone! il y a environ 10 heures" [ref=e346]:
- generic [ref=e347]:
- img [ref=e348]
- heading "Test Note" [level=3] [ref=e351]
- paragraph [ref=e352]: This is my first note to test the Google Keep clone!
- generic [ref=e353]: il y a environ 10 heures
- generic [ref=e354]:
- button "Pin" [ref=e355]:
- img
- button "Change color" [ref=e356]:
- img
- button [ref=e357]:
- img
- button "Titre Modifié Contenu modifié avec succès! il y a environ 5 heures" [ref=e358]:
- generic [ref=e359]:
- heading "Titre Modifié" [level=3] [ref=e360]
- paragraph [ref=e361]: Contenu modifié avec succès!
- generic [ref=e362]: il y a environ 5 heures
- generic [ref=e363]:
- button "Pin" [ref=e364]:
- img
- button "Change color" [ref=e365]:
- img
- button [ref=e366]:
- img
- status [ref=e367]
- generic [ref=e368]:
- generic [ref=e369]:
- generic [ref=e370]: Note created successfully
- button [ref=e371]:
- img [ref=e372]
- generic [ref=e375]:
- generic [ref=e376]: Note created successfully
- button [ref=e377]:
- img [ref=e378]
- generic [ref=e381]:
- generic [ref=e382]: Note created successfully
- button [ref=e383]:
- img [ref=e384]
- generic [ref=e387]:
- generic [ref=e388]: Note created successfully
- button [ref=e389]:
- img [ref=e390]
- button "Open Next.js Dev Tools" [ref=e398] [cursor=pointer]:
- img [ref=e399]
- alert [ref=e402]
```

View File

@@ -1,509 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e3]:
- link "Memento" [ref=e4] [cursor=pointer]:
- /url: /
- img [ref=e5]
- generic [ref=e8]: Memento
- generic [ref=e10]:
- img [ref=e11]
- textbox "Search notes..." [ref=e14]
- button [ref=e15]:
- img
- navigation [ref=e16]:
- link "Notes" [ref=e17] [cursor=pointer]:
- /url: /
- img [ref=e18]
- text: Notes
- link "Archive" [ref=e21] [cursor=pointer]:
- /url: /archive
- img [ref=e22]
- text: Archive
- main [ref=e25]:
- generic [ref=e27]:
- textbox "Take a note..." [ref=e28]
- button "New checklist" [ref=e29]:
- img
- generic [ref=e30]:
- generic [ref=e31]:
- heading "Pinned" [level=2] [ref=e32]
- button "Updated Note avec image il y a environ 8 heures" [ref=e34]:
- generic [ref=e35]:
- img [ref=e36]
- heading "Updated" [level=3] [ref=e38]
- paragraph [ref=e39]: Note avec image
- generic [ref=e40]: il y a environ 8 heures
- generic [ref=e41]:
- button "Unpin" [ref=e42]:
- img
- button "Change color" [ref=e43]:
- img
- button [ref=e44]:
- img
- generic [ref=e45]:
- heading "Others" [level=2] [ref=e46]
- generic [ref=e47]:
- button "test-1767557339218-Note 4 test-1767557339218-Content 4 il y a moins dune minute" [ref=e48]:
- generic [ref=e49]:
- heading "test-1767557339218-Note 4" [level=3] [ref=e50]
- paragraph [ref=e51]: test-1767557339218-Content 4
- generic [ref=e52]: il y a moins dune minute
- generic [ref=e53]:
- button "Pin" [ref=e54]:
- img
- button "Change color" [ref=e55]:
- img
- button [ref=e56]:
- img
- button "test-1767557339218-Note 3 test-1767557339218-Content 3 il y a moins dune minute" [ref=e57]:
- generic [ref=e58]:
- heading "test-1767557339218-Note 3" [level=3] [ref=e59]
- paragraph [ref=e60]: test-1767557339218-Content 3
- generic [ref=e61]: il y a moins dune minute
- generic [ref=e62]:
- button "Pin" [ref=e63]:
- img
- button "Change color" [ref=e64]:
- img
- button [ref=e65]:
- img
- button "test-1767557339218-Note 2 test-1767557339218-Content 2 il y a moins dune minute" [ref=e66]:
- generic [ref=e67]:
- heading "test-1767557339218-Note 2" [level=3] [ref=e68]
- paragraph [ref=e69]: test-1767557339218-Content 2
- generic [ref=e70]: il y a moins dune minute
- generic [ref=e71]:
- button "Pin" [ref=e72]:
- img
- button "Change color" [ref=e73]:
- img
- button [ref=e74]:
- img
- button "test-1767557339218-Note 1 test-1767557339218-Content 1 il y a moins dune minute" [ref=e75]:
- generic [ref=e76]:
- heading "test-1767557339218-Note 1" [level=3] [ref=e77]
- paragraph [ref=e78]: test-1767557339218-Content 1
- generic [ref=e79]: il y a moins dune minute
- generic [ref=e80]:
- button "Pin" [ref=e81]:
- img
- button "Change color" [ref=e82]:
- img
- button [ref=e83]:
- img
- button "test-1767557334587-Note 4 test-1767557334587-Content 4 il y a moins dune minute" [ref=e84]:
- generic [ref=e85]:
- heading "test-1767557334587-Note 4" [level=3] [ref=e86]
- paragraph [ref=e87]: test-1767557334587-Content 4
- generic [ref=e88]: il y a moins dune minute
- generic [ref=e89]:
- button "Pin" [ref=e90]:
- img
- button "Change color" [ref=e91]:
- img
- button [ref=e92]:
- img
- button "test-1767557334587-Note 3 test-1767557334587-Content 3 il y a moins dune minute" [ref=e93]:
- generic [ref=e94]:
- heading "test-1767557334587-Note 3" [level=3] [ref=e95]
- paragraph [ref=e96]: test-1767557334587-Content 3
- generic [ref=e97]: il y a moins dune minute
- generic [ref=e98]:
- button "Pin" [ref=e99]:
- img
- button "Change color" [ref=e100]:
- img
- button [ref=e101]:
- img
- button "test-1767557334587-Note 2 test-1767557334587-Content 2 il y a moins dune minute" [ref=e102]:
- generic [ref=e103]:
- heading "test-1767557334587-Note 2" [level=3] [ref=e104]
- paragraph [ref=e105]: test-1767557334587-Content 2
- generic [ref=e106]: il y a moins dune minute
- generic [ref=e107]:
- button "Pin" [ref=e108]:
- img
- button "Change color" [ref=e109]:
- img
- button [ref=e110]:
- img
- button "test-1767557334587-Note 1 test-1767557334587-Content 1 il y a moins dune minute" [ref=e111]:
- generic [ref=e112]:
- heading "test-1767557334587-Note 1" [level=3] [ref=e113]
- paragraph [ref=e114]: test-1767557334587-Content 1
- generic [ref=e115]: il y a moins dune minute
- generic [ref=e116]:
- button "Pin" [ref=e117]:
- img
- button "Change color" [ref=e118]:
- img
- button [ref=e119]:
- img
- button "test-1767557330820-Note 4 test-1767557330820-Content 4 il y a moins dune minute" [ref=e120]:
- generic [ref=e121]:
- heading "test-1767557330820-Note 4" [level=3] [ref=e122]
- paragraph [ref=e123]: test-1767557330820-Content 4
- generic [ref=e124]: il y a moins dune minute
- generic [ref=e125]:
- button "Pin" [ref=e126]:
- img
- button "Change color" [ref=e127]:
- img
- button [ref=e128]:
- img
- button "test-1767557330820-Note 3 test-1767557330820-Content 3 il y a moins dune minute" [ref=e129]:
- generic [ref=e130]:
- heading "test-1767557330820-Note 3" [level=3] [ref=e131]
- paragraph [ref=e132]: test-1767557330820-Content 3
- generic [ref=e133]: il y a moins dune minute
- generic [ref=e134]:
- button "Pin" [ref=e135]:
- img
- button "Change color" [ref=e136]:
- img
- button [ref=e137]:
- img
- button "test-1767557330820-Note 2 test-1767557330820-Content 2 il y a moins dune minute" [ref=e138]:
- generic [ref=e139]:
- heading "test-1767557330820-Note 2" [level=3] [ref=e140]
- paragraph [ref=e141]: test-1767557330820-Content 2
- generic [ref=e142]: il y a moins dune minute
- generic [ref=e143]:
- button "Pin" [ref=e144]:
- img
- button "Change color" [ref=e145]:
- img
- button [ref=e146]:
- img
- button "test-1767557330820-Note 1 test-1767557330820-Content 1 il y a moins dune minute" [ref=e147]:
- generic [ref=e148]:
- heading "test-1767557330820-Note 1" [level=3] [ref=e149]
- paragraph [ref=e150]: test-1767557330820-Content 1
- generic [ref=e151]: il y a moins dune minute
- generic [ref=e152]:
- button "Pin" [ref=e153]:
- img
- button "Change color" [ref=e154]:
- img
- button [ref=e155]:
- img
- button "test-1767557327567-Note 4 test-1767557327567-Content 4 il y a moins dune minute" [ref=e156]:
- generic [ref=e157]:
- heading "test-1767557327567-Note 4" [level=3] [ref=e158]
- paragraph [ref=e159]: test-1767557327567-Content 4
- generic [ref=e160]: il y a moins dune minute
- generic [ref=e161]:
- button "Pin" [ref=e162]:
- img
- button "Change color" [ref=e163]:
- img
- button [ref=e164]:
- img
- button "test-1767557327567-Note 3 test-1767557327567-Content 3 il y a moins dune minute" [ref=e165]:
- generic [ref=e166]:
- heading "test-1767557327567-Note 3" [level=3] [ref=e167]
- paragraph [ref=e168]: test-1767557327567-Content 3
- generic [ref=e169]: il y a moins dune minute
- generic [ref=e170]:
- button "Pin" [ref=e171]:
- img
- button "Change color" [ref=e172]:
- img
- button [ref=e173]:
- img
- button "test-1767557327567-Note 2 test-1767557327567-Content 2 il y a moins dune minute" [ref=e174]:
- generic [ref=e175]:
- heading "test-1767557327567-Note 2" [level=3] [ref=e176]
- paragraph [ref=e177]: test-1767557327567-Content 2
- generic [ref=e178]: il y a moins dune minute
- generic [ref=e179]:
- button "Pin" [ref=e180]:
- img
- button "Change color" [ref=e181]:
- img
- button [ref=e182]:
- img
- button "test-1767557327567-Note 1 test-1767557327567-Content 1 il y a moins dune minute" [ref=e183]:
- generic [ref=e184]:
- heading "test-1767557327567-Note 1" [level=3] [ref=e185]
- paragraph [ref=e186]: test-1767557327567-Content 1
- generic [ref=e187]: il y a moins dune minute
- generic [ref=e188]:
- button "Pin" [ref=e189]:
- img
- button "Change color" [ref=e190]:
- img
- button [ref=e191]:
- img
- button "test-1767557324248-Note 4 test-1767557324248-Content 4 il y a moins dune minute" [ref=e192]:
- generic [ref=e193]:
- heading "test-1767557324248-Note 4" [level=3] [ref=e194]
- paragraph [ref=e195]: test-1767557324248-Content 4
- generic [ref=e196]: il y a moins dune minute
- generic [ref=e197]:
- button "Pin" [ref=e198]:
- img
- button "Change color" [ref=e199]:
- img
- button [ref=e200]:
- img
- button "test-1767557324248-Note 3 test-1767557324248-Content 3 il y a moins dune minute" [ref=e201]:
- generic [ref=e202]:
- heading "test-1767557324248-Note 3" [level=3] [ref=e203]
- paragraph [ref=e204]: test-1767557324248-Content 3
- generic [ref=e205]: il y a moins dune minute
- generic [ref=e206]:
- button "Pin" [ref=e207]:
- img
- button "Change color" [ref=e208]:
- img
- button [ref=e209]:
- img
- button "test-1767557324248-Note 2 test-1767557324248-Content 2 il y a moins dune minute" [ref=e210]:
- generic [ref=e211]:
- heading "test-1767557324248-Note 2" [level=3] [ref=e212]
- paragraph [ref=e213]: test-1767557324248-Content 2
- generic [ref=e214]: il y a moins dune minute
- generic [ref=e215]:
- button "Pin" [ref=e216]:
- img
- button "Change color" [ref=e217]:
- img
- button [ref=e218]:
- img
- button "test-1767557324248-Note 1 test-1767557324248-Content 1 il y a moins dune minute" [ref=e219]:
- generic [ref=e220]:
- heading "test-1767557324248-Note 1" [level=3] [ref=e221]
- paragraph [ref=e222]: test-1767557324248-Content 1
- generic [ref=e223]: il y a moins dune minute
- generic [ref=e224]:
- button "Pin" [ref=e225]:
- img
- button "Change color" [ref=e226]:
- img
- button [ref=e227]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e228]:
- generic [ref=e229]:
- heading "Test Note for Reminder" [level=3] [ref=e230]
- paragraph [ref=e231]: This note will have a reminder
- generic [ref=e232]: il y a 26 minutes
- generic [ref=e233]:
- button "Pin" [ref=e234]:
- img
- button "Change color" [ref=e235]:
- img
- button [ref=e236]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e237]:
- generic [ref=e238]:
- heading "Test Note for Reminder" [level=3] [ref=e239]
- paragraph [ref=e240]: This note will have a reminder
- generic [ref=e241]: il y a 26 minutes
- generic [ref=e242]:
- button "Pin" [ref=e243]:
- img
- button "Change color" [ref=e244]:
- img
- button [ref=e245]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e246]:
- generic [ref=e247]:
- heading "Test Note for Reminder" [level=3] [ref=e248]
- paragraph [ref=e249]: This note will have a reminder
- generic [ref=e250]: il y a 26 minutes
- generic [ref=e251]:
- button "Pin" [ref=e252]:
- img
- button "Change color" [ref=e253]:
- img
- button [ref=e254]:
- img
- button "Test note il y a 26 minutes" [ref=e255]:
- generic [ref=e256]:
- paragraph [ref=e257]: Test note
- generic [ref=e258]: il y a 26 minutes
- generic [ref=e259]:
- button "Pin" [ref=e260]:
- img
- button "Change color" [ref=e261]:
- img
- button [ref=e262]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e263]:
- generic [ref=e264]:
- heading "Test Note for Reminder" [level=3] [ref=e265]
- paragraph [ref=e266]: This note will have a reminder
- generic [ref=e267]: il y a 26 minutes
- generic [ref=e268]:
- button "Pin" [ref=e269]:
- img
- button "Change color" [ref=e270]:
- img
- button [ref=e271]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e272]:
- generic [ref=e273]:
- heading "Test Note for Reminder" [level=3] [ref=e274]
- paragraph [ref=e275]: This note will have a reminder
- generic [ref=e276]: il y a 26 minutes
- generic [ref=e277]:
- button "Pin" [ref=e278]:
- img
- button "Change color" [ref=e279]:
- img
- button [ref=e280]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e281]:
- generic [ref=e282]:
- heading "Test Note for Reminder" [level=3] [ref=e283]
- paragraph [ref=e284]: This note will have a reminder
- generic [ref=e285]: il y a 26 minutes
- generic [ref=e286]:
- button "Pin" [ref=e287]:
- img
- button "Change color" [ref=e288]:
- img
- button [ref=e289]:
- img
- button "Test Note for Reminder This note will have a reminder il y a 26 minutes" [ref=e290]:
- generic [ref=e291]:
- img [ref=e292]
- heading "Test Note for Reminder" [level=3] [ref=e295]
- paragraph [ref=e296]: This note will have a reminder
- generic [ref=e297]: il y a 26 minutes
- generic [ref=e298]:
- button "Pin" [ref=e299]:
- img
- button "Change color" [ref=e300]:
- img
- button [ref=e301]:
- img
- button "test sample file il y a environ 5 heures" [ref=e302]:
- generic [ref=e303]:
- heading "test" [level=3] [ref=e304]
- paragraph [ref=e306]: sample file
- generic [ref=e307]: il y a environ 5 heures
- generic [ref=e308]:
- button "Pin" [ref=e309]:
- img
- button "Change color" [ref=e310]:
- img
- button [ref=e311]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d''un framework pour l''IA - Optimisation de l''entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l''inférence en temps réel Tags: framework, mlops, gpu, ai, tech tech ai framework mlops gpu il y a environ 5 heures" [ref=e312]':
- generic [ref=e313]:
- heading "New AI Framework Released" [level=3] [ref=e314]
- paragraph [ref=e315]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and is engineered for performance and scalability. It includes features for simplifying model deployment and enhancing capabilities for real-time inference. Pourquoi cest IA/DevIA: - Conception d'un framework pour l'IA - Optimisation de l'entraînement des modèles - Support pour les réseaux de neurones - Prise en charge de l'inférence en temps réel Tags: framework, mlops, gpu, ai, tech"
- generic [ref=e316]:
- generic [ref=e317]: tech
- generic [ref=e318]: ai
- generic [ref=e319]: framework
- generic [ref=e320]: mlops
- generic [ref=e321]: gpu
- generic [ref=e322]: il y a environ 5 heures
- generic [ref=e323]:
- button "Pin" [ref=e324]:
- img
- button "Change color" [ref=e325]:
- img
- button [ref=e326]:
- img
- button "Test Image API Note avec image il y a environ 8 heures" [ref=e327]:
- generic [ref=e328]:
- heading "Test Image API" [level=3] [ref=e329]
- paragraph [ref=e331]: Note avec image
- generic [ref=e332]: il y a environ 8 heures
- generic [ref=e333]:
- button "Pin" [ref=e334]:
- img
- button "Change color" [ref=e335]:
- img
- button [ref=e336]:
- img
- button "Test Markdown Titre Modifié Sous-titre édité Liste modifiée 1 Liste modifiée 2 Nouvelle liste 3 Texte gras modifié et italique édité console.log(\"Code modifié avec succès!\") il y a environ 5 heures" [ref=e337]:
- generic [ref=e338]:
- heading "Test Markdown" [level=3] [ref=e339]
- generic [ref=e341]:
- heading "Titre Modifié" [level=1] [ref=e342]
- heading "Sous-titre édité" [level=2] [ref=e343]
- list [ref=e344]:
- listitem [ref=e345]: Liste modifiée 1
- listitem [ref=e346]: Liste modifiée 2
- listitem [ref=e347]: Nouvelle liste 3
- paragraph [ref=e348]:
- strong [ref=e349]: Texte gras modifié
- text: et
- emphasis [ref=e350]: italique édité
- code [ref=e352]: console.log("Code modifié avec succès!")
- generic [ref=e353]: il y a environ 5 heures
- generic [ref=e354]:
- button "Pin" [ref=e355]:
- img
- button "Change color" [ref=e356]:
- img
- button [ref=e357]:
- img
- button "Test Image Avec image il y a environ 8 heures" [ref=e358]:
- generic [ref=e359]:
- heading "Test Image" [level=3] [ref=e360]
- paragraph [ref=e361]: Avec image
- generic [ref=e362]: il y a environ 8 heures
- generic [ref=e363]:
- button "Pin" [ref=e364]:
- img
- button "Change color" [ref=e365]:
- img
- button [ref=e366]:
- img
- 'button "New AI Framework Released Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu tech ai framework mlops gpu il y a environ 6 heures" [ref=e367]':
- generic [ref=e368]:
- heading "New AI Framework Released" [level=3] [ref=e369]
- paragraph [ref=e370]: "Lien: <www.example.com> Date: 2026-01-04 Résumé: A new AI framework designed for optimized training and deployment of machine learning models has been released. The framework supports a variety of neural networks and includes features that enhance computational efficiency and model accuracy. This release aims to simplify the integration of AI tools into existing systems, making it easier for developers to build robust AI applications. Pourquoi cest IA/DevIA: - Optimized training methods for machine learning models. - Supports deployment across various hardware configurations. - Focus on enhancing computational efficiency and model performance. - Intended for use in real-world AI applications and development. Tags: tech, ai, framework, mlops, gpu"
- generic [ref=e371]:
- generic [ref=e372]: tech
- generic [ref=e373]: ai
- generic [ref=e374]: framework
- generic [ref=e375]: mlops
- generic [ref=e376]: gpu
- generic [ref=e377]: il y a environ 6 heures
- generic [ref=e378]:
- button "Pin" [ref=e379]:
- img
- button "Change color" [ref=e380]:
- img
- button [ref=e381]:
- img
- button "Test Note This is my first note to test the Google Keep clone! il y a environ 10 heures" [ref=e382]:
- generic [ref=e383]:
- img [ref=e384]
- heading "Test Note" [level=3] [ref=e387]
- paragraph [ref=e388]: This is my first note to test the Google Keep clone!
- generic [ref=e389]: il y a environ 10 heures
- generic [ref=e390]:
- button "Pin" [ref=e391]:
- img
- button "Change color" [ref=e392]:
- img
- button [ref=e393]:
- img
- button "Titre Modifié Contenu modifié avec succès! il y a environ 5 heures" [ref=e394]:
- generic [ref=e395]:
- heading "Titre Modifié" [level=3] [ref=e396]
- paragraph [ref=e397]: Contenu modifié avec succès!
- generic [ref=e398]: il y a environ 5 heures
- generic [ref=e399]:
- button "Pin" [ref=e400]:
- img
- button "Change color" [ref=e401]:
- img
- button [ref=e402]:
- img
- status [ref=e403]
- button "Open Next.js Dev Tools" [ref=e409] [cursor=pointer]:
- img [ref=e410]
- alert [ref=e413]
```

View File

@@ -0,0 +1,9 @@
import { test, expect } from '@playwright/test';
test('capture masonry layout', async ({ page }) => {
await page.goto('http://localhost:3000');
// Attendre que Muuri s'initialise et que le layout se stabilise
await page.waitForTimeout(2000);
await page.screenshot({ path: '.playwright-mcp/muuri-masonry-layout.png', fullPage: true });
console.log('Screenshot saved to .playwright-mcp/muuri-masonry-layout.png');
});

2
keep-notes/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare module 'web-animations-js';
declare module 'muuri';