Connect app to MySQL database at 192.168.4.20

This commit is contained in:
2025-12-01 18:45:48 +00:00
commit 0cfcd05921
16 changed files with 6302 additions and 0 deletions

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Dependencies
/node_modules
/.pnp
.pnp.js
# 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
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
# Environment
.env

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# Ultimate Lists App
A powerful, self-hosted todo list application built with Next.js that works as a Progressive Web App (PWA).
## Features
- 📱 Works as a Safari web app (no App Store needed)
- 🗄️ Self-hosted with SQLite database
- ✅ Multiple lists with custom colors
- 📋 Rich todo items with descriptions, priorities, and due dates
- 🎨 Clean, responsive design
- ⚡ Fast server-side rendering
## Getting Started
1. Install dependencies:
```bash
npm install
```
2. Set up the database:
```bash
npm run db:push
```
3. Run the development server:
```bash
npm run dev
```
4. Open [http://localhost:3000](http://localhost:3000) in Safari
## Adding to Home Screen (Safari)
1. Open the app in Safari
2. Tap the Share button
3. Select "Add to Home Screen"
4. The app will now work like a native app!
## Deployment
Build for production:
```bash
npm run build
npm start
```
The app uses SQLite, so no external database setup is required.
## Tech Stack
- **Next.js 14** - React framework with App Router
- **Prisma** - Database ORM
- **SQLite** - File-based database
- **Tailwind CSS** - Styling
- **TypeScript** - Type safety
- **Lucide React** - Icons

3
app/globals.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

22
app/layout.tsx Normal file
View File

@@ -0,0 +1,22 @@
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Ultimate Lists',
description: 'The ultimate todo list app',
manifest: '/manifest.json',
themeColor: '#3B82F6',
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className="bg-gray-50 min-h-screen">{children}</body>
</html>
)
}

32
app/page.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { PlusIcon } from 'lucide-react'
import { getLists } from '@/lib/actions'
import ListCard from '@/components/ListCard'
export default async function Home() {
const lists = await getLists()
return (
<main className="container mx-auto px-4 py-8 max-w-4xl">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-gray-900">Ultimate Lists</h1>
<button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
<PlusIcon size={20} />
New List
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{lists.map((list) => (
<ListCard key={list.id} list={list} />
))}
</div>
{lists.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 text-lg mb-4">No lists yet</p>
<p className="text-gray-400">Create your first list to get started</p>
</div>
)}
</main>
)
}

49
components/ListCard.tsx Normal file
View File

@@ -0,0 +1,49 @@
import Link from 'next/link'
import { CheckCircle2, Circle } from 'lucide-react'
interface List {
id: string
title: string
color: string
items: Array<{
id: string
completed: boolean
}>
}
export default function ListCard({ list }: { list: List }) {
const completedCount = list.items.filter(item => item.completed).length
const totalCount = list.items.length
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
return (
<Link href={`/list/${list.id}`}>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow cursor-pointer">
<div className="flex items-center gap-3 mb-4">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: list.color }}
/>
<h3 className="font-semibold text-gray-900 truncate">{list.title}</h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between text-sm text-gray-600">
<span>{completedCount} of {totalCount} completed</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="h-2 rounded-full transition-all duration-300"
style={{
width: `${progress}%`,
backgroundColor: list.color
}}
/>
</div>
</div>
</div>
</Link>
)
}

63
lib/actions.ts Normal file
View File

@@ -0,0 +1,63 @@
'use server'
import { prisma } from './db'
import { revalidatePath } from 'next/cache'
export async function getLists() {
return await prisma.list.findMany({
include: {
items: true,
},
orderBy: {
updatedAt: 'desc',
},
})
}
export async function createList(title: string, color: string = '#3B82F6') {
const list = await prisma.list.create({
data: {
title,
color,
},
})
revalidatePath('/')
return list
}
export async function getListWithItems(listId: string) {
return await prisma.list.findUnique({
where: { id: listId },
include: {
items: {
orderBy: {
createdAt: 'desc',
},
},
},
})
}
export async function createItem(listId: string, title: string, description?: string) {
const item = await prisma.item.create({
data: {
title,
description,
listId,
},
})
revalidatePath(`/list/${listId}`)
return item
}
export async function toggleItem(itemId: string) {
const item = await prisma.item.findUnique({ where: { id: itemId } })
if (!item) return null
const updated = await prisma.item.update({
where: { id: itemId },
data: { completed: !item.completed },
})
revalidatePath(`/list/${item.listId}`)
return updated
}

9
lib/db.ts Normal file
View File

@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

8
next.config.js Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
}
module.exports = nextConfig

5893
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "ultimate-lists-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:push": "prisma db push",
"db:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.7.1",
"lucide-react": "^0.294.0",
"mysql2": "^3.15.3",
"next": "14.0.4",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.4",
"postcss": "^8",
"prisma": "^5.7.1",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

30
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,30 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model List {
id String @id @default(cuid())
title String
color String @default("#3B82F6")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items Item[]
}
model Item {
id String @id @default(cuid())
title String
description String?
completed Boolean @default(false)
priority String @default("medium") // low, medium, high
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
listId String
list List @relation(fields: [listId], references: [id], onDelete: Cascade)
}

21
public/manifest.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "Ultimate Lists",
"short_name": "Lists",
"description": "The ultimate todo list app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3B82F6",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

12
tailwind.config.js Normal file
View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}