init
This commit is contained in:
commit
61f6235445
14 changed files with 3366 additions and 0 deletions
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# Testing
|
||||
/coverage
|
||||
|
||||
# Next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
70
app/api/tasks/[id]/route.ts
Normal file
70
app/api/tasks/[id]/route.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { toggleAufgabe, deleteAufgabe, getAufgabeById } from '@/lib/db';
|
||||
|
||||
// PUT /api/tasks/:id - Aufgabe als erledigt/rückgängig markieren
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const id = parseInt(params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Ungültige Aufgaben-ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const updatedAufgabe = await toggleAufgabe(id);
|
||||
|
||||
if (!updatedAufgabe) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Aufgabe nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(updatedAufgabe);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Aktualisieren der Aufgabe' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/tasks/:id - Aufgabe löschen
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const id = parseInt(params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Ungültige Aufgaben-ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const deleted = await deleteAufgabe(id);
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Aufgabe nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Löschen der Aufgabe' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
39
app/api/tasks/export/route.ts
Normal file
39
app/api/tasks/export/route.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getAufgaben } from '@/lib/db';
|
||||
|
||||
// CSV-Header definieren
|
||||
const CSV_HEADER = 'ID,Beschreibung,Erledigt,Erstellt\n';
|
||||
|
||||
// Aufgabe in CSV-Zeile konvertieren
|
||||
function aufgabeToCsvRow(aufgabe: any): string {
|
||||
const erledigt = aufgabe.erledigt ? 'Ja' : 'Nein';
|
||||
const erstellt = new Date(aufgabe.erstellt).toLocaleString('de-DE');
|
||||
// Kommas in der Beschreibung durch Semikolon ersetzen und Anführungszeichen escapen
|
||||
const beschreibung = `"${aufgabe.beschreibung.replace(/"/g, '""')}"`;
|
||||
return `${aufgabe.id},${beschreibung},${erledigt},"${erstellt}"`;
|
||||
}
|
||||
|
||||
// GET /api/tasks/export - CSV-Export
|
||||
export async function GET() {
|
||||
try {
|
||||
const aufgaben = await getAufgaben();
|
||||
|
||||
// CSV generieren
|
||||
const csvRows = aufgaben.map(aufgabeToCsvRow);
|
||||
const csvContent = CSV_HEADER + csvRows.join('\n');
|
||||
|
||||
// CSV als Datei-Download zurückgeben
|
||||
return new NextResponse(csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="aufgaben.csv"',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim CSV-Export:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Erstellen des CSV-Exports' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
56
app/api/tasks/route.ts
Normal file
56
app/api/tasks/route.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getAufgaben, addAufgabe, getAufgabenCount } from '@/lib/db';
|
||||
|
||||
// GET /api/tasks - Aufgaben mit Paginierung und Sortierung abrufen
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '10');
|
||||
const sort = searchParams.get('sort') as 'text_asc' | 'text_desc' | 'datum_asc' | 'datum_desc' || 'datum_desc';
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const aufgaben = await getAufgaben(limit, offset, sort);
|
||||
const total = await getAufgabenCount();
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return NextResponse.json({
|
||||
aufgaben,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Abrufen der Aufgaben' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/tasks - Neue Aufgabe hinzufügen
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { beschreibung } = await request.json();
|
||||
|
||||
if (!beschreibung || typeof beschreibung !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Beschreibung ist erforderlich' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const neueAufgabe = await addAufgabe(beschreibung.trim());
|
||||
return NextResponse.json(neueAufgabe, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Hinzufügen der Aufgabe' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
210
app/globals.css
Normal file
210
app/globals.css
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-group button {
|
||||
padding: 10px 20px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-group button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.todo-item.completed {
|
||||
background-color: #e8f5e9;
|
||||
text-decoration: line-through;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.todo-item .content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.todo-item span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.todo-item .datum {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.todo-item .actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.todo-item .actions a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.erledigen-btn {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loeschen-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.erledigen-btn:hover {
|
||||
background-color: #0b7dda;
|
||||
}
|
||||
|
||||
.loeschen-btn:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
.sort-dropdown {
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
padding: 5px 15px;
|
||||
background-color: #9C27B0;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.export-button:hover:not(:disabled) {
|
||||
background-color: #7B1FA2;
|
||||
}
|
||||
|
||||
.export-button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sort-container {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copyright p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 8px 16px;
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled) {
|
||||
background-color: #0b7dda;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
19
app/layout.tsx
Normal file
19
app/layout.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'To-Do-Liste',
|
||||
description: 'Einfache Todo-App mit Next.js und TypeScript',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
241
app/page.tsx
Normal file
241
app/page.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface Aufgabe {
|
||||
id: number;
|
||||
beschreibung: string;
|
||||
erledigt: boolean;
|
||||
erstellt: string;
|
||||
}
|
||||
|
||||
interface Pagination {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
type SortOption = 'text_asc' | 'text_desc' | 'datum_asc' | 'datum_desc';
|
||||
|
||||
export default function TodoApp() {
|
||||
const [aufgaben, setAufgaben] = useState<Aufgabe[]>([]);
|
||||
const [neueAufgabe, setNeueAufgabe] = useState('');
|
||||
const [sortierung, setSortierung] = useState<SortOption>('datum_desc');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
totalPages: 1
|
||||
});
|
||||
|
||||
// Lade Aufgaben vom Server mit Paginierung und Sortierung
|
||||
const loadAufgaben = async (page: number = 1) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(`/api/tasks?page=${page}&limit=10&sort=${sortierung}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAufgaben(data.aufgaben);
|
||||
setPagination(data.pagination);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Aufgaben:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial laden
|
||||
useEffect(() => {
|
||||
loadAufgaben(1);
|
||||
}, []);
|
||||
|
||||
// Aufgaben werden bereits serverseitig sortiert, keine clientseitige Sortierung nötig
|
||||
const sortierteAufgaben = aufgaben;
|
||||
|
||||
const handleHinzufuegen = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (neueAufgabe.trim()) {
|
||||
try {
|
||||
const response = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ beschreibung: neueAufgabe.trim() }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setNeueAufgabe('');
|
||||
// Zur ersten Seite zurück, wenn neue Aufgabe hinzugefügt wird
|
||||
await loadAufgaben(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoeschen = async (id: number) => {
|
||||
if (confirm('Möchtest du diese Aufgabe wirklich löschen?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Aktuelle Seite neu laden oder zur vorherigen, wenn leer
|
||||
await loadAufgaben(pagination.page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleErledigen = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${id}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadAufgaben(pagination.page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Sortierung ändern und Aufgaben neu laden
|
||||
const handleSortierungAendern = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const neueSortierung = e.target.value as SortOption;
|
||||
setSortierung(neueSortierung);
|
||||
await loadAufgaben(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>To-Do-Liste</h1>
|
||||
|
||||
{/* Sortieroptionen und Export */}
|
||||
<div className="sort-container">
|
||||
<span>Sortieren nach:</span>
|
||||
<select
|
||||
value={sortierung}
|
||||
onChange={handleSortierungAendern}
|
||||
className="sort-dropdown"
|
||||
>
|
||||
<option value="text_asc">Name (A-Z)</option>
|
||||
<option value="text_desc">Name (Z-A)</option>
|
||||
<option value="datum_asc">Datum (älteste)</option>
|
||||
<option value="datum_desc">Datum (neueste)</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => window.location.href = '/api/tasks/export'}
|
||||
className="export-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
CSV Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Formular zum Hinzufügen */}
|
||||
<form onSubmit={handleHinzufuegen} className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
value={neueAufgabe}
|
||||
onChange={(e) => setNeueAufgabe(e.target.value)}
|
||||
placeholder="Neue Aufgabe eingeben..."
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button type="submit" disabled={isLoading}>
|
||||
{isLoading ? 'Lädt...' : 'Hinzufügen'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* To-Do-Liste */}
|
||||
{isLoading ? (
|
||||
<p className="empty-message">Lädt Aufgaben...</p>
|
||||
) : sortierteAufgaben.length > 0 ? (
|
||||
<ul className="todo-list">
|
||||
{sortierteAufgaben.map((aufgabe) => (
|
||||
<li
|
||||
key={aufgabe.id}
|
||||
className={`todo-item ${aufgabe.erledigt ? 'completed' : ''}`}
|
||||
>
|
||||
<div className="content">
|
||||
<span>{aufgabe.beschreibung}</span>
|
||||
<span className="datum">
|
||||
{new Date(aufgabe.erstellt).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<a
|
||||
href="#"
|
||||
className="erledigen-btn"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleErledigen(aufgabe.id);
|
||||
}}
|
||||
title={aufgabe.erledigt ? 'Rückgängig' : 'Erledigt'}
|
||||
>
|
||||
{aufgabe.erledigt ? '↩' : '✓'}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="loeschen-btn"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleLoeschen(aufgabe.id);
|
||||
}}
|
||||
title="Löschen"
|
||||
>
|
||||
❌
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="empty-message">
|
||||
Die To-Do-Liste ist leer. Füge eine neue Aufgabe hinzu!
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Paginierung */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="pagination">
|
||||
<button
|
||||
onClick={() => loadAufgaben(Math.max(1, pagination.page - 1))}
|
||||
disabled={pagination.page <= 1 || isLoading}
|
||||
>
|
||||
← Zurück
|
||||
</button>
|
||||
<span className="pagination-info">
|
||||
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Aufgaben)
|
||||
</span>
|
||||
<button
|
||||
onClick={() => loadAufgaben(Math.min(pagination.totalPages, pagination.page + 1))}
|
||||
disabled={pagination.page >= pagination.totalPages || isLoading}
|
||||
>
|
||||
Weiter →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="copyright">
|
||||
<p>© 2026 GSTools</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
coolify.yaml
Normal file
13
coolify.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: todo-app
|
||||
type: node
|
||||
buildCommand: npm install && npm run build
|
||||
startCommand: npm run start
|
||||
port: 3000
|
||||
|
||||
# WICHTIG: Volume für SQLite-Datenbank
|
||||
volumes:
|
||||
- ./todo.db:/app/todo.db
|
||||
|
||||
# Automatisches Deployment bei Git-Push
|
||||
autoDeployment: true
|
||||
branch: main
|
||||
120
lib/db.ts
Normal file
120
lib/db.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import sqlite3 from 'sqlite3';
|
||||
import { open, Database } from 'sqlite';
|
||||
import path from 'path';
|
||||
|
||||
// Datenbank-Datei-Pfad
|
||||
const dbPath = path.join(process.cwd(), 'todo.db');
|
||||
|
||||
// Datenbank-Instanz (wird asynchron initialisiert)
|
||||
let db: Database | null = null;
|
||||
|
||||
// Datenbank öffnen
|
||||
export async function initDb(): Promise<Database> {
|
||||
if (db) return db;
|
||||
db = await open({
|
||||
filename: dbPath,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
// Tabelle erstellen, falls nicht vorhanden
|
||||
await db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS aufgaben (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
beschreibung TEXT NOT NULL,
|
||||
erledigt INTEGER DEFAULT 0,
|
||||
erstellt TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
// Typdefinition
|
||||
export interface Aufgabe {
|
||||
id: number;
|
||||
beschreibung: string;
|
||||
erledigt: boolean;
|
||||
erstellt: string;
|
||||
}
|
||||
|
||||
type SortOption = 'text_asc' | 'text_desc' | 'datum_asc' | 'datum_desc';
|
||||
|
||||
// Sortier-Order für SQLite
|
||||
function getOrderBy(sort: SortOption): string {
|
||||
switch (sort) {
|
||||
case 'text_asc':
|
||||
return 'beschreibung ASC';
|
||||
case 'text_desc':
|
||||
return 'beschreibung DESC';
|
||||
case 'datum_asc':
|
||||
return 'erstellt ASC';
|
||||
case 'datum_desc':
|
||||
default:
|
||||
return 'erstellt DESC';
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Aufgaben abrufen (mit Paginierung und Sortierung)
|
||||
export async function getAufgaben(limit: number = 1000, offset: number = 0, sort: SortOption = 'datum_desc'): Promise<Aufgabe[]> {
|
||||
const database = await initDb();
|
||||
const orderBy = getOrderBy(sort);
|
||||
const rows = await database.all(
|
||||
`SELECT * FROM aufgaben ORDER BY ${orderBy} LIMIT ? OFFSET ?`,
|
||||
[limit, offset]
|
||||
) as any[];
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
erledigt: Boolean(row.erledigt)
|
||||
}));
|
||||
}
|
||||
|
||||
// Anzahl aller Aufgaben abrufen
|
||||
export async function getAufgabenCount(): Promise<number> {
|
||||
const database = await initDb();
|
||||
const result = await database.get('SELECT COUNT(*) as count FROM aufgaben') as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
// Aufgabe hinzufügen
|
||||
export async function addAufgabe(beschreibung: string): Promise<Aufgabe> {
|
||||
const database = await initDb();
|
||||
const result = await database.run(
|
||||
'INSERT INTO aufgaben (beschreibung, erledigt, erstellt) VALUES (?, 0, datetime("now"))',
|
||||
[beschreibung]
|
||||
);
|
||||
const newId = result.lastID as number;
|
||||
return getAufgabeById(newId)!;
|
||||
}
|
||||
|
||||
// Aufgabe nach ID abrufen
|
||||
export async function getAufgabeById(id: number): Promise<Aufgabe | null> {
|
||||
const database = await initDb();
|
||||
const row = await database.get('SELECT * FROM aufgaben WHERE id = ?', id) as any;
|
||||
if (!row) return null;
|
||||
return {
|
||||
...row,
|
||||
erledigt: Boolean(row.erledigt)
|
||||
};
|
||||
}
|
||||
|
||||
// Aufgabe als erledigt/rückgängig markieren
|
||||
export async function toggleAufgabe(id: number): Promise<Aufgabe | null> {
|
||||
const database = await initDb();
|
||||
await database.run('UPDATE aufgaben SET erledigt = 1 - erledigt WHERE id = ?', id);
|
||||
return getAufgabeById(id);
|
||||
}
|
||||
|
||||
// Aufgabe löschen
|
||||
export async function deleteAufgabe(id: number): Promise<boolean> {
|
||||
const database = await initDb();
|
||||
const result = await database.run('DELETE FROM aufgaben WHERE id = ?', id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
// Datenbank schließen
|
||||
export async function closeDatabase() {
|
||||
if (db) {
|
||||
await db.close();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
4
next.config.mjs
Normal file
4
next.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
2494
package-lock.json
generated
Normal file
2494
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "nextjs-todo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.2.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
BIN
todo.db
Normal file
BIN
todo.db
Normal file
Binary file not shown.
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue