Add BMAD framework, authentication, and new features
This commit is contained in:
16
keep-notes/.dockerignore
Normal file
16
keep-notes/.dockerignore
Normal 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
60
keep-notes/Dockerfile
Normal 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"]
|
||||
29
keep-notes/app/actions/auth.ts
Normal file
29
keep-notes/app/actions/auth.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
54
keep-notes/app/actions/register.ts
Normal file
54
keep-notes/app/actions/register.ts
Normal 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');
|
||||
}
|
||||
54
keep-notes/app/actions/scrape.ts
Normal file
54
keep-notes/app/actions/scrape.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
31
keep-notes/app/api/admin/randomize-labels/route.ts
Normal file
31
keep-notes/app/api/admin/randomize-labels/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
57
keep-notes/app/api/admin/sync-labels/route.ts
Normal file
57
keep-notes/app/api/admin/sync-labels/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
2
keep-notes/app/api/auth/[...nextauth]/route.ts
Normal file
2
keep-notes/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { handlers } from "@/auth"
|
||||
export const { GET, POST } = handlers
|
||||
62
keep-notes/app/api/cron/reminders/route.ts
Normal file
62
keep-notes/app/api/cron/reminders/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
39
keep-notes/app/api/upload/route.ts
Normal file
39
keep-notes/app/api/upload/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
11
keep-notes/app/login/page.tsx
Normal file
11
keep-notes/app/login/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
11
keep-notes/app/register/page.tsx
Normal file
11
keep-notes/app/register/page.tsx
Normal 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
43
keep-notes/auth.config.ts
Normal 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
52
keep-notes/auth.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
33
keep-notes/components/editor-images.tsx
Normal file
33
keep-notes/components/editor-images.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
160
keep-notes/components/label-management-dialog.tsx
Normal file
160
keep-notes/components/label-management-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
86
keep-notes/components/login-form.tsx
Normal file
86
keep-notes/components/login-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
203
keep-notes/components/masonry-grid.tsx
Normal file
203
keep-notes/components/masonry-grid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
112
keep-notes/components/note-actions.tsx
Normal file
112
keep-notes/components/note-actions.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
42
keep-notes/components/note-checklist.tsx
Normal file
42
keep-notes/components/note-checklist.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
68
keep-notes/components/note-images.tsx
Normal file
68
keep-notes/components/note-images.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
104
keep-notes/components/register-form.tsx
Normal file
104
keep-notes/components/register-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
102
keep-notes/components/reminder-dialog.tsx
Normal file
102
keep-notes/components/reminder-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
110
keep-notes/components/sidebar.tsx
Normal file
110
keep-notes/components/sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
140
keep-notes/components/ui/sheet.tsx
Normal file
140
keep-notes/components/ui/sheet.tsx
Normal 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,
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
23
keep-notes/docker-compose.yml
Normal file
23
keep-notes/docker-compose.yml
Normal 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"
|
||||
45
keep-notes/hooks/use-reminder-check.ts
Normal file
45
keep-notes/hooks/use-reminder-check.ts
Normal 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]);
|
||||
}
|
||||
33
keep-notes/hooks/use-resize-observer.ts
Normal file
33
keep-notes/hooks/use-resize-observer.ts
Normal 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;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
9
keep-notes/middleware.ts
Normal 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$).*)'],
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
5788
keep-notes/package-lock.json
generated
5788
keep-notes/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 c’est 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 c’est 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 c’est 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 c’est 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]
|
||||
```
|
||||
@@ -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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 c’est 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 c’est 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 c’est 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 c’est 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]
|
||||
```
|
||||
@@ -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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 c’est 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 c’est 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 c’est 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 c’est 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
@@ -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.
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "links" TEXT;
|
||||
@@ -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");
|
||||
@@ -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 |
11
keep-notes/scripts/check-users.js
Normal file
11
keep-notes/scripts/check-users.js
Normal 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());
|
||||
12
keep-notes/scripts/check-users.ts
Normal file
12
keep-notes/scripts/check-users.ts
Normal 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())
|
||||
22
keep-notes/scripts/seed-user.ts
Normal file
22
keep-notes/scripts/seed-user.ts
Normal 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())
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"1e19528dd527cbd19c0c-4c49b0bcb3667bb1a228",
|
||||
"1e19528dd527cbd19c0c-039ef3f0ab8fb4aa0094",
|
||||
"1e19528dd527cbd19c0c-ff6161ab584bdf7fa93d"
|
||||
]
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 c’est 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 c’est 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 c’est 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 c’est 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]
|
||||
```
|
||||
@@ -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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 c’est 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 c’est 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 c’est 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 c’est 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]
|
||||
```
|
||||
@@ -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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 d’une 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 c’est 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 c’est 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 c’est 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 c’est 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]
|
||||
```
|
||||
9
keep-notes/tests/capture-masonry.spec.ts
Normal file
9
keep-notes/tests/capture-masonry.spec.ts
Normal 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
2
keep-notes/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare module 'web-animations-js';
|
||||
declare module 'muuri';
|
||||
Reference in New Issue
Block a user