[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

@ -49,6 +49,7 @@
"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",
@ -62,6 +63,7 @@
"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",
@ -78,10 +80,12 @@
"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",

33
pnpm-lock.yaml generated
View File

@ -83,6 +83,9 @@ dependencies:
cmdk:
specifier: ^0.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:
specifier: ^3.3.1
version: 3.3.1
@ -122,6 +125,9 @@ dependencies:
nodemailer:
specifier: ^6.9.8
version: 6.9.8
papaparse:
specifier: ^5.5.2
version: 5.5.2
react:
specifier: 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)
devDependencies:
'@types/csv-parse':
specifier: ^1.2.5
version: 1.2.5
'@types/eslint':
specifier: ^8.44.7
version: 8.56.2
@ -178,6 +187,9 @@ devDependencies:
'@types/nodemailer':
specifier: ^6.4.15
version: 6.4.15
'@types/papaparse':
specifier: ^5.3.15
version: 5.3.15
'@types/react':
specifier: ^18.2.37
version: 18.2.48
@ -4348,6 +4360,13 @@ packages:
'@types/estree': 1.0.5
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:
resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==}
dev: false
@ -4481,6 +4500,12 @@ packages:
'@types/node': 18.19.8
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:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@ -4781,6 +4806,7 @@ packages:
/acorn-import-assertions@1.9.0(acorn@8.11.3):
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
deprecated: package has been renamed to acorn-import-attributes
peerDependencies:
acorn: ^8
dependencies:
@ -5554,6 +5580,9 @@ packages:
/csstype@3.1.3:
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):
resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
peerDependencies:
@ -8993,6 +9022,10 @@ packages:
engines: {node: '>=6'}
dev: false
/papaparse@5.5.2:
resolution: {integrity: sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==}
dev: false
/parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}

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>