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

@ -49,6 +49,7 @@
"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",
"csv-parse": "^5.6.0",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"framer-motion": "^11.0.3", "framer-motion": "^11.0.3",
"geist": "^1.2.1", "geist": "^1.2.1",
@ -62,6 +63,7 @@
"nextra": "^2.13.4", "nextra": "^2.13.4",
"nextra-theme-blog": "^2.13.4", "nextra-theme-blog": "^2.13.4",
"nodemailer": "^6.9.8", "nodemailer": "^6.9.8",
"papaparse": "^5.5.2",
"react": "18.2.0", "react": "18.2.0",
"react-day-picker": "^8.10.0", "react-day-picker": "^8.10.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -78,10 +80,12 @@
"zustand": "^4.5.0" "zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/csv-parse": "^1.2.5",
"@types/eslint": "^8.44.7", "@types/eslint": "^8.44.7",
"@types/next-pwa": "^5.6.9", "@types/next-pwa": "^5.6.9",
"@types/node": "^18.17.0", "@types/node": "^18.17.0",
"@types/nodemailer": "^6.4.15", "@types/nodemailer": "^6.4.15",
"@types/papaparse": "^5.3.15",
"@types/react": "^18.2.37", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.15",
"@types/web-push": "^3.6.3", "@types/web-push": "^3.6.3",

33
pnpm-lock.yaml generated
View File

@ -83,6 +83,9 @@ dependencies:
cmdk: cmdk:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) version: 0.2.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
csv-parse:
specifier: ^5.6.0
version: 5.6.0
date-fns: date-fns:
specifier: ^3.3.1 specifier: ^3.3.1
version: 3.3.1 version: 3.3.1
@ -122,6 +125,9 @@ dependencies:
nodemailer: nodemailer:
specifier: ^6.9.8 specifier: ^6.9.8
version: 6.9.8 version: 6.9.8
papaparse:
specifier: ^5.5.2
version: 5.5.2
react: react:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0 version: 18.2.0
@ -166,6 +172,9 @@ dependencies:
version: 4.5.0(@types/react@18.2.48)(react@18.2.0) version: 4.5.0(@types/react@18.2.48)(react@18.2.0)
devDependencies: devDependencies:
'@types/csv-parse':
specifier: ^1.2.5
version: 1.2.5
'@types/eslint': '@types/eslint':
specifier: ^8.44.7 specifier: ^8.44.7
version: 8.56.2 version: 8.56.2
@ -178,6 +187,9 @@ devDependencies:
'@types/nodemailer': '@types/nodemailer':
specifier: ^6.4.15 specifier: ^6.4.15
version: 6.4.15 version: 6.4.15
'@types/papaparse':
specifier: ^5.3.15
version: 5.3.15
'@types/react': '@types/react':
specifier: ^18.2.37 specifier: ^18.2.37
version: 18.2.48 version: 18.2.48
@ -4348,6 +4360,13 @@ packages:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
dev: false dev: false
/@types/csv-parse@1.2.5:
resolution: {integrity: sha512-3PoFyWeuFGqale09vFydLQ6IGdvD+mizcXcB8s6ImWv+830IF0HckvewgcGVfGnTFImqvfvhpYZYod2QqGGGdg==}
deprecated: This is a stub types definition. csv-parse provides its own type definitions, so you do not need this installed.
dependencies:
csv-parse: 5.6.0
dev: true
/@types/d3-scale-chromatic@3.0.3: /@types/d3-scale-chromatic@3.0.3:
resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==}
dev: false dev: false
@ -4481,6 +4500,12 @@ packages:
'@types/node': 18.19.8 '@types/node': 18.19.8
dev: true dev: true
/@types/papaparse@5.3.15:
resolution: {integrity: sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==}
dependencies:
'@types/node': 18.19.8
dev: true
/@types/prop-types@15.7.11: /@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@ -4781,6 +4806,7 @@ packages:
/acorn-import-assertions@1.9.0(acorn@8.11.3): /acorn-import-assertions@1.9.0(acorn@8.11.3):
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
deprecated: package has been renamed to acorn-import-attributes
peerDependencies: peerDependencies:
acorn: ^8 acorn: ^8
dependencies: dependencies:
@ -5554,6 +5580,9 @@ packages:
/csstype@3.1.3: /csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
/csv-parse@5.6.0:
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
/cytoscape-cose-bilkent@4.1.0(cytoscape@3.28.1): /cytoscape-cose-bilkent@4.1.0(cytoscape@3.28.1):
resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
peerDependencies: peerDependencies:
@ -8993,6 +9022,10 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: false dev: false
/papaparse@5.5.2:
resolution: {integrity: sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==}
dev: false
/parent-module@1.0.1: /parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}

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>