Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
max_line_length = off
trim_trailing_whitespace = false
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Database (SQLite)
DATABASE_URL="file:./prisma/dev.db"

# Timezone
TZ=Asia/Shanghai

# NextAuth (set in production)
# NEXTAUTH_URL=https://your-domain.com
# NEXTAUTH_SECRET=replace-with-strong-secret

# Optional Email provider for magic link
# EMAIL_SERVER=smtp://user:pass@smtp.example.com:587
# EMAIL_FROM=noreply@example.com
15 changes: 15 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['next/core-web-vitals', 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'react/jsx-key': 'off'
},
ignorePatterns: ['*.js', 'node_modules/', 'dist/', '.next/']
};
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
branches: ["**"]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile || yarn install
- name: Generate Prisma Client
run: npx prisma generate
- name: Lint
run: yarn lint
- name: Typecheck
run: yarn typecheck
60 changes: 39 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
### AL ###
#Template for AL projects for Dynamics 365 Business Central
#launch.json folder
# Dependencies
node_modules/

# Next.js
.next/
out/

# Production
build/

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# Env
.env
.env.local
.env.*.local

# OS
.DS_Store

# Prisma
prisma/dev.db
prisma/dev.db-journal

# Typescript
*.tsbuildinfo

# Editor
.vscode/
#Cache folder
.alcache/
#Symbols folder
.alpackages/
#Snapshots folder
.snapshots/
#Testing Output folder
.output/
#Extension App-file
*.app
#Rapid Application Development File
rad.json
#Translation Base-file
*.g.xlf
#License-file
*.flf
#Test results file
TestResults.xml
.idea/

# Tailwind JIT cache
.next/cache/

# Husky
.husky/_/
5 changes: 5 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint
yarn typecheck
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}
32 changes: 32 additions & 0 deletions messages/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"app": {
"title": "示例应用",
"nav": {
"dashboard": "仪表盘",
"market": "市场",
"signIn": "登录",
"signOut": "退出登录"
}
},
"dashboard": {
"title": "仪表盘",
"welcome": "欢迎使用!当前为中文(中国)界面。"
},
"market": {
"title": "市场:{symbol}",
"price": "价格",
"sample": "示例:¥{amount}"
},
"auth": {
"signInTitle": "登录",
"email": "邮箱",
"password": "密码",
"submit": "登录",
"or": "或",
"magicLink": "发送登录链接到邮箱"
},
"common": {
"currency": "人民币",
"hello": "你好"
}
}
5 changes: 5 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
13 changes: 13 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
typedRoutes: true
}
};

export default withNextIntl(nextConfig);
54 changes: 54 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "nextjs-zhcn-cny-prisma-sqlite",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"seed": "prisma db seed",
"prepare": "husky install",
"postinstall": "prisma generate"
},
"dependencies": {
"@prisma/client": "^5.16.2",
"big.js": "^6.2.1",
"date-fns": "^3.6.0",
"dayjs": "^1.11.13",
"next": "^14.2.7",
"next-auth": "^4.24.7",
"next-intl": "^3.15.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^20.11.30",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.11.0",
"@typescript-eslint/parser": "^8.11.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "^14.2.7",
"eslint-config-prettier": "^9.1.0",
"husky": "^9.1.6",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prisma": "^5.16.2",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
},
"prisma": {
"seed": "node prisma/seed.js"
},
"engines": {
"node": ">=18.18.0"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
15 changes: 15 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = "file:./dev.db"
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
}
28 changes: 28 additions & 0 deletions prisma/seed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Simple seed script to populate a couple of users
const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

async function main() {
const existing = await prisma.user.findMany();
if (existing.length === 0) {
await prisma.user.createMany({
data: [
{ email: 'alice@example.com', name: 'Alice' },
{ email: 'bob@example.com', name: 'Bob' }
]
});
console.log('Seeded users');
} else {
console.log('Users already exist, skipping seed');
}
}

main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
82 changes: 82 additions & 0 deletions src/app/[locale]/auth/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import { useState, FormEvent } from 'react';
import { signIn } from 'next-auth/react';

export default function SignInPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);

async function onSubmit(e: FormEvent) {
e.preventDefault();
setLoading(true);
setMessage(null);
const res = await signIn('credentials', {
email,
password,
redirect: false
});
setLoading(false);
if (res?.ok) {
setMessage('登录成功');
} else {
setMessage(res?.error || '登录失败');
}
}

async function sendMagicLink() {
setLoading(true);
setMessage(null);
const res = await signIn('email', { email, redirect: false });
setLoading(false);
if (res?.ok) setMessage('如果已配置邮件服务,登录链接已发送到邮箱');
else setMessage(res?.error || '无法发送登录链接');
}

return (
<div className="max-w-sm mx-auto space-y-4">
<h1 className="text-2xl font-bold">登录</h1>
<form onSubmit={onSubmit} className="space-y-3">
<div>
<label className="block text-sm mb-1">邮箱</label>
<input
className="w-full px-3 py-2 rounded bg-gray-800 border border-gray-700"
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="you@example.com"
required
/>
</div>
<div>
<label className="block text-sm mb-1">密码</label>
<input
className="w-full px-3 py-2 rounded bg-gray-800 border border-gray-700"
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="••••••••"
/>
</div>
<button
disabled={loading}
className="w-full py-2 rounded bg-blue-600 hover:bg-blue-500 disabled:opacity-50"
type="submit"
>
{loading ? '登录中…' : '登录'}
</button>
</form>
<div className="text-center text-sm opacity-70">或</div>
<button
onClick={sendMagicLink}
disabled={loading || !email}
className="w-full py-2 rounded bg-gray-700 hover:bg-gray-600 disabled:opacity-50"
>
发送登录链接到邮箱
</button>
{message && <div className="text-sm opacity-80">{message}</div>}
</div>
);
}
Loading
Loading