[feat] Can choose csv file, and add relation pkg.

This commit is contained in:
kk034kk034 2025-05-01 23:08:14 +08:00
parent 6142b8b2b5
commit 9a181e0867
4 changed files with 11394 additions and 11336 deletions

View File

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

View File

@ -1,108 +1,112 @@
{
"name": "split",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"just-build": "next build",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:dev": "prisma migrate dev",
"db:seed": "prisma db seed",
"prisma:prod": "prisma migrate deploy",
"dev": "next dev",
"postinstall": "prisma generate",
"generate": "prisma generate",
"lint": "next lint",
"start": "sleep 3 && pnpm prisma:prod && next start",
"start-with-latest-migrations": "prisma migrate deploy && next start",
"d": "pnpm dx && pnpm dev",
"dx": "pnpm i && pnpm dx:up && pnpm db:dev",
"dx:up": "docker compose -f docker/dev/compose.yml up -d",
"dx:down": "docker compose -f docker/dev/compose.yml down"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.515.0",
"@aws-sdk/s3-request-presigner": "^3.515.0",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.9.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.43.6",
"@trpc/next": "^10.43.6",
"@trpc/react-query": "^10.43.6",
"@trpc/server": "^10.43.6",
"babel-loader": "^9.1.3",
"boring-avatars": "^1.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"date-fns": "^3.3.1",
"framer-motion": "^11.0.3",
"geist": "^1.2.1",
"input-otp": "^1.2.3",
"lucide-react": "^0.312.0",
"nanoid": "^5.0.6",
"next": "^14.0.4",
"next-auth": "^4.24.5",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
"nextra": "^2.13.4",
"nextra-theme-blog": "^2.13.4",
"nodemailer": "^6.9.8",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"resend": "^3.2.0",
"sharp": "0.32.6",
"sonner": "^1.4.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.8.9",
"web-push": "^3.6.7",
"zod": "^3.22.4",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/eslint": "^8.44.7",
"@types/next-pwa": "^5.6.9",
"@types/node": "^18.17.0",
"@types/nodemailer": "^6.4.15",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/web-push": "^3.6.3",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.4",
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.9.1",
"tailwindcss": "^3.3.5",
"tsx": "^4.7.1",
"typescript": "^5.1.6"
},
"ct3aMetadata": {
"initVersion": "7.25.2"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"packageManager": "pnpm@8.9.2"
{
"name": "split",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"just-build": "next build",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:dev": "prisma migrate dev",
"db:seed": "prisma db seed",
"prisma:prod": "prisma migrate deploy",
"dev": "next dev",
"postinstall": "prisma generate",
"generate": "prisma generate",
"lint": "next lint",
"start": "sleep 3 && pnpm prisma:prod && next start",
"start-with-latest-migrations": "prisma migrate deploy && next start",
"d": "pnpm dx && pnpm dev",
"dx": "pnpm i && pnpm dx:up && pnpm db:dev",
"dx:up": "docker compose -f docker/dev/compose.yml up -d",
"dx:down": "docker compose -f docker/dev/compose.yml down"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.515.0",
"@aws-sdk/s3-request-presigner": "^3.515.0",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.9.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.43.6",
"@trpc/next": "^10.43.6",
"@trpc/react-query": "^10.43.6",
"@trpc/server": "^10.43.6",
"babel-loader": "^9.1.3",
"boring-avatars": "^1.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"csv-parse": "^5.6.0",
"date-fns": "^3.3.1",
"framer-motion": "^11.0.3",
"geist": "^1.2.1",
"input-otp": "^1.2.3",
"lucide-react": "^0.312.0",
"nanoid": "^5.0.6",
"next": "^14.0.4",
"next-auth": "^4.24.5",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
"nextra": "^2.13.4",
"nextra-theme-blog": "^2.13.4",
"nodemailer": "^6.9.8",
"papaparse": "^5.5.2",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"resend": "^3.2.0",
"sharp": "0.32.6",
"sonner": "^1.4.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.8.9",
"web-push": "^3.6.7",
"zod": "^3.22.4",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/csv-parse": "^1.2.5",
"@types/eslint": "^8.44.7",
"@types/next-pwa": "^5.6.9",
"@types/node": "^18.17.0",
"@types/nodemailer": "^6.4.15",
"@types/papaparse": "^5.3.15",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/web-push": "^3.6.3",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.4",
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.9.1",
"tailwindcss": "^3.3.5",
"tsx": "^4.7.1",
"typescript": "^5.1.6"
},
"ct3aMetadata": {
"initVersion": "7.25.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 { useRouter } from 'next/router';
import { PaperClipIcon } from '@heroicons/react/24/solid';
import Papa from 'papaparse';
const ImportSpliwisePage: NextPageWithUser = () => {
const [usersWithBalance, setUsersWithBalance] = useState<Array<SplitwiseUser>>([]);
@ -35,42 +36,61 @@ const ImportSpliwisePage: NextPageWithUser = () => {
setUploadedFile(file);
try {
const json = JSON.parse(await file.text()) as Record<string, unknown>;
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);
}
}
const text = await file.text();
setUsersWithBalance(friendsWithOutStandingBalance);
setSelectedUsers(
friendsWithOutStandingBalance.reduce(
(acc, user) => {
if (file.name.endsWith('.json')) {
const json = JSON.parse(text) as Record<string, unknown>;
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);
setSelectedUsers(
friendsWithOutStandingBalance.reduce((acc, user) => {
acc[user.id] = true;
return acc;
},
{} as Record<string, boolean>,
),
);
}, {} as Record<string, boolean>)
);
const _groups = (json.groups as Array<SplitwiseGroup>).filter(
(g) => g.members.length > 0 && g.id !== 0,
);
const _groups = (json.groups as Array<SplitwiseGroup>).filter(
(g) => g.members.length > 0 && g.id !== 0
);
setGroups(_groups);
setSelectedGroups(
_groups.reduce(
(acc, group) => {
setGroups(_groups);
setSelectedGroups(
_groups.reduce((acc, group) => {
acc[group.id] = true;
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) {
console.error(e);
toast.error('Error importing file');
@ -124,7 +144,7 @@ const ImportSpliwisePage: NextPageWithUser = () => {
</div>
</div>
<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 items-center border-r pr-4 ">
<PaperClipIcon className="mr-2 h-4 w-4" />{' '}
@ -136,9 +156,9 @@ const ImportSpliwisePage: NextPageWithUser = () => {
</div>
<Input
onChange={handleFileChange}
id="splitwise-json"
id="splitwise-data"
type="file"
accept=".json"
accept=".json,.csv,text/csv"
className="hidden"
/>
</label>