Compare commits
1 Commits
main
...
feat/impor
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a181e0867 |
@ -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
|
||||||
|
|
||||||
|
|||||||
218
package.json
218
package.json
@ -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
22429
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user