Compare commits

..

1 Commits

Author SHA1 Message Date
9a181e0867 [feat] Can choose csv file, and add relation pkg. 2025-05-01 23:08:14 +08:00
4 changed files with 11394 additions and 11336 deletions

View File

@ -50,6 +50,7 @@ pnpm i
- Must: DATABASE_URL - Must: DATABASE_URL
```bash ```bash
pnpm db:dev # init schema pnpm db:dev # init schema
pnpm prisma migrate deploy # A new database 'splitpro' created
``` ```
- Option: R2 related - Option: R2 related

View File

@ -1,108 +1,112 @@
{ {
"name": "split", "name": "split",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"just-build": "next build", "just-build": "next build",
"db:push": "prisma db push", "db:push": "prisma db push",
"db:studio": "prisma studio", "db:studio": "prisma studio",
"db:dev": "prisma migrate dev", "db:dev": "prisma migrate dev",
"db:seed": "prisma db seed", "db:seed": "prisma db seed",
"prisma:prod": "prisma migrate deploy", "prisma:prod": "prisma migrate deploy",
"dev": "next dev", "dev": "next dev",
"postinstall": "prisma generate", "postinstall": "prisma generate",
"generate": "prisma generate", "generate": "prisma generate",
"lint": "next lint", "lint": "next lint",
"start": "sleep 3 && pnpm prisma:prod && next start", "start": "sleep 3 && pnpm prisma:prod && next start",
"start-with-latest-migrations": "prisma migrate deploy && next start", "start-with-latest-migrations": "prisma migrate deploy && next start",
"d": "pnpm dx && pnpm dev", "d": "pnpm dx && pnpm dev",
"dx": "pnpm i && pnpm dx:up && pnpm db:dev", "dx": "pnpm i && pnpm dx:up && pnpm db:dev",
"dx:up": "docker compose -f docker/dev/compose.yml up -d", "dx:up": "docker compose -f docker/dev/compose.yml up -d",
"dx:down": "docker compose -f docker/dev/compose.yml down" "dx:down": "docker compose -f docker/dev/compose.yml down"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.515.0", "@aws-sdk/client-s3": "^3.515.0",
"@aws-sdk/s3-request-presigner": "^3.515.0", "@aws-sdk/s3-request-presigner": "^3.515.0",
"@heroicons/react": "^2.1.1", "@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.9.1", "@prisma/client": "^5.9.1",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@t3-oss/env-nextjs": "^0.11.1", "@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.43.6", "@trpc/client": "^10.43.6",
"@trpc/next": "^10.43.6", "@trpc/next": "^10.43.6",
"@trpc/react-query": "^10.43.6", "@trpc/react-query": "^10.43.6",
"@trpc/server": "^10.43.6", "@trpc/server": "^10.43.6",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"boring-avatars": "^1.10.1", "boring-avatars": "^1.10.1",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"date-fns": "^3.3.1", "csv-parse": "^5.6.0",
"framer-motion": "^11.0.3", "date-fns": "^3.3.1",
"geist": "^1.2.1", "framer-motion": "^11.0.3",
"input-otp": "^1.2.3", "geist": "^1.2.1",
"lucide-react": "^0.312.0", "input-otp": "^1.2.3",
"nanoid": "^5.0.6", "lucide-react": "^0.312.0",
"next": "^14.0.4", "nanoid": "^5.0.6",
"next-auth": "^4.24.5", "next": "^14.0.4",
"next-pwa": "^5.6.0", "next-auth": "^4.24.5",
"next-themes": "^0.2.1", "next-pwa": "^5.6.0",
"nextra": "^2.13.4", "next-themes": "^0.2.1",
"nextra-theme-blog": "^2.13.4", "nextra": "^2.13.4",
"nodemailer": "^6.9.8", "nextra-theme-blog": "^2.13.4",
"react": "18.2.0", "nodemailer": "^6.9.8",
"react-day-picker": "^8.10.0", "papaparse": "^5.5.2",
"react-dom": "18.2.0", "react": "18.2.0",
"react-hook-form": "^7.50.1", "react-day-picker": "^8.10.0",
"resend": "^3.2.0", "react-dom": "18.2.0",
"sharp": "0.32.6", "react-hook-form": "^7.50.1",
"sonner": "^1.4.0", "resend": "^3.2.0",
"superjson": "^2.2.1", "sharp": "0.32.6",
"tailwind-merge": "^2.2.0", "sonner": "^1.4.0",
"tailwindcss-animate": "^1.0.7", "superjson": "^2.2.1",
"vaul": "^0.8.9", "tailwind-merge": "^2.2.0",
"web-push": "^3.6.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4", "vaul": "^0.8.9",
"zustand": "^4.5.0" "web-push": "^3.6.7",
}, "zod": "^3.22.4",
"devDependencies": { "zustand": "^4.5.0"
"@types/eslint": "^8.44.7", },
"@types/next-pwa": "^5.6.9", "devDependencies": {
"@types/node": "^18.17.0", "@types/csv-parse": "^1.2.5",
"@types/nodemailer": "^6.4.15", "@types/eslint": "^8.44.7",
"@types/react": "^18.2.37", "@types/next-pwa": "^5.6.9",
"@types/react-dom": "^18.2.15", "@types/node": "^18.17.0",
"@types/web-push": "^3.6.3", "@types/nodemailer": "^6.4.15",
"@typescript-eslint/eslint-plugin": "^6.11.0", "@types/papaparse": "^5.3.15",
"@typescript-eslint/parser": "^6.11.0", "@types/react": "^18.2.37",
"autoprefixer": "^10.4.14", "@types/react-dom": "^18.2.15",
"eslint": "^8.54.0", "@types/web-push": "^3.6.3",
"eslint-config-next": "^14.0.4", "@typescript-eslint/eslint-plugin": "^6.11.0",
"postcss": "^8.4.31", "@typescript-eslint/parser": "^6.11.0",
"prettier": "^3.1.0", "autoprefixer": "^10.4.14",
"prettier-plugin-tailwindcss": "^0.5.7", "eslint": "^8.54.0",
"prisma": "^5.9.1", "eslint-config-next": "^14.0.4",
"tailwindcss": "^3.3.5", "postcss": "^8.4.31",
"tsx": "^4.7.1", "prettier": "^3.1.0",
"typescript": "^5.1.6" "prettier-plugin-tailwindcss": "^0.5.7",
}, "prisma": "^5.9.1",
"ct3aMetadata": { "tailwindcss": "^3.3.5",
"initVersion": "7.25.2" "tsx": "^4.7.1",
}, "typescript": "^5.1.6"
"prisma": { },
"seed": "tsx prisma/seed.ts" "ct3aMetadata": {
}, "initVersion": "7.25.2"
"packageManager": "pnpm@8.9.2" },
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"packageManager": "pnpm@8.9.2"
} }

22429
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ import { DownloadCloud } from 'lucide-react';
import { LoadingSpinner } from '~/components/ui/spinner'; import { LoadingSpinner } from '~/components/ui/spinner';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { PaperClipIcon } from '@heroicons/react/24/solid'; import { PaperClipIcon } from '@heroicons/react/24/solid';
import Papa from 'papaparse';
const ImportSpliwisePage: NextPageWithUser = () => { const ImportSpliwisePage: NextPageWithUser = () => {
const [usersWithBalance, setUsersWithBalance] = useState<Array<SplitwiseUser>>([]); const [usersWithBalance, setUsersWithBalance] = useState<Array<SplitwiseUser>>([]);
@ -35,42 +36,61 @@ const ImportSpliwisePage: NextPageWithUser = () => {
setUploadedFile(file); setUploadedFile(file);
try { try {
const json = JSON.parse(await file.text()) as Record<string, unknown>; const text = await file.text();
const friendsWithOutStandingBalance: Array<SplitwiseUser> = [];
for (const friend of json.friends as Array<Record<string, unknown>>) {
const balance = friend.balance as Array<{ currency_code: string; amount: string }>;
if (balance.length && friend.registration_status === 'confirmed') {
friendsWithOutStandingBalance.push(friend as SplitwiseUser);
}
}
setUsersWithBalance(friendsWithOutStandingBalance); if (file.name.endsWith('.json')) {
setSelectedUsers( const json = JSON.parse(text) as Record<string, unknown>;
friendsWithOutStandingBalance.reduce( const friendsWithOutStandingBalance: Array<SplitwiseUser> = [];
(acc, user) => {
for (const friend of json.friends as Array<Record<string, unknown>>) {
const balance = friend.balance as Array<{ currency_code: string; amount: string }>;
if (balance.length && friend.registration_status === 'confirmed') {
friendsWithOutStandingBalance.push(friend as SplitwiseUser);
}
}
setUsersWithBalance(friendsWithOutStandingBalance);
setSelectedUsers(
friendsWithOutStandingBalance.reduce((acc, user) => {
acc[user.id] = true; acc[user.id] = true;
return acc; return acc;
}, }, {} as Record<string, boolean>)
{} as Record<string, boolean>, );
),
);
const _groups = (json.groups as Array<SplitwiseGroup>).filter( const _groups = (json.groups as Array<SplitwiseGroup>).filter(
(g) => g.members.length > 0 && g.id !== 0, (g) => g.members.length > 0 && g.id !== 0
); );
setGroups(_groups); setGroups(_groups);
setSelectedGroups( setSelectedGroups(
_groups.reduce( _groups.reduce((acc, group) => {
(acc, group) => {
acc[group.id] = true; acc[group.id] = true;
return acc; return acc;
}, }, {} as Record<string, boolean>)
{} as Record<string, boolean>, );
),
);
console.log('Friends with outstanding balance', friendsWithOutStandingBalance); console.log('Parsed JSON: Friends and groups imported');
} else if (file.name.endsWith('.csv')) {
const result = Papa.parse(text, {
header: true,
skipEmptyLines: true,
});
if (result.errors.length > 0) {
console.error(result.errors);
toast.error('Error parsing CSV file');
return;
}
const parsed = result.data as Array<Record<string, string>>;
console.log('Parsed CSV data:', parsed.slice(0, 5)); // 預覽前 5 筆
// ⚠️ TODO你可以根據 parsed 建立對應的 users / groups 結構
toast.success('CSV loaded, please implement parsing logic.');
} else {
toast.error('Unsupported file format. Please upload .json or .csv');
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
toast.error('Error importing file'); toast.error('Error importing file');
@ -124,7 +144,7 @@ const ImportSpliwisePage: NextPageWithUser = () => {
</div> </div>
</div> </div>
<div className="mt-4 flex items-center gap-4"> <div className="mt-4 flex items-center gap-4">
<label htmlFor="splitwise-json" className="w-full cursor-pointer rounded border"> <label htmlFor="splitwise-data" className="w-full cursor-pointer rounded border">
<div className="flex cursor-pointer px-3 py-[6px] "> <div className="flex cursor-pointer px-3 py-[6px] ">
<div className="flex items-center border-r pr-4 "> <div className="flex items-center border-r pr-4 ">
<PaperClipIcon className="mr-2 h-4 w-4" />{' '} <PaperClipIcon className="mr-2 h-4 w-4" />{' '}
@ -136,9 +156,9 @@ const ImportSpliwisePage: NextPageWithUser = () => {
</div> </div>
<Input <Input
onChange={handleFileChange} onChange={handleFileChange}
id="splitwise-json" id="splitwise-data"
type="file" type="file"
accept=".json" accept=".json,.csv,text/csv"
className="hidden" className="hidden"
/> />
</label> </label>