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
|
||||||
|
|
||||||
|
|||||||
@ -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
33
pnpm-lock.yaml
generated
@ -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'}
|
||||||
|
|||||||
@ -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,8 +36,12 @@ 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();
|
||||||
|
|
||||||
|
if (file.name.endsWith('.json')) {
|
||||||
|
const json = JSON.parse(text) as Record<string, unknown>;
|
||||||
const friendsWithOutStandingBalance: Array<SplitwiseUser> = [];
|
const friendsWithOutStandingBalance: Array<SplitwiseUser> = [];
|
||||||
|
|
||||||
for (const friend of json.friends as Array<Record<string, unknown>>) {
|
for (const friend of json.friends as Array<Record<string, unknown>>) {
|
||||||
const balance = friend.balance as Array<{ currency_code: string; amount: string }>;
|
const balance = friend.balance as Array<{ currency_code: string; amount: string }>;
|
||||||
if (balance.length && friend.registration_status === 'confirmed') {
|
if (balance.length && friend.registration_status === 'confirmed') {
|
||||||
@ -46,31 +51,46 @@ const ImportSpliwisePage: NextPageWithUser = () => {
|
|||||||
|
|
||||||
setUsersWithBalance(friendsWithOutStandingBalance);
|
setUsersWithBalance(friendsWithOutStandingBalance);
|
||||||
setSelectedUsers(
|
setSelectedUsers(
|
||||||
friendsWithOutStandingBalance.reduce(
|
friendsWithOutStandingBalance.reduce((acc, user) => {
|
||||||
(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