Skip to content

Commit 0bf10d5

Browse files
authored
Merge pull request #51 from meceware/dev
v1.2.0
2 parents b5f7f2a + 0171740 commit 0bf10d5

19 files changed

+1069
-536
lines changed

package-lock.json

+308-287
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "wapy.dev",
33
"description": "Track, manage, and optimize your recurring expenses in one powerful and human readable dashboard. Never miss a payment again.",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"private": true,
66
"repository": "https://github.com/meceware/wapy.dev.git",
77
"scripts": {
@@ -16,7 +16,7 @@
1616
"dependencies": {
1717
"@auth/prisma-adapter": "^2.7.4",
1818
"@date-fns/tz": "^1.2.0",
19-
"@hookform/resolvers": "^3.10.0",
19+
"@hookform/resolvers": "^4.1.0",
2020
"@paddle/paddle-js": "^1.3.3",
2121
"@paddle/paddle-node-sdk": "^2.5.0",
2222
"@prisma/client": "^6.3.1",
@@ -37,7 +37,7 @@
3737
"@radix-ui/react-toggle": "^1.1.2",
3838
"@radix-ui/react-toggle-group": "^1.1.2",
3939
"@radix-ui/react-tooltip": "^1.1.8",
40-
"@tailwindcss/postcss": "^4.0.4",
40+
"@tailwindcss/postcss": "^4.0.6",
4141
"class-variance-authority": "^0.7.1",
4242
"clsx": "^2.1.1",
4343
"cmdk": "^1.0.4",
@@ -50,7 +50,7 @@
5050
"jsonwebtoken": "^9.0.2",
5151
"lodash.debounce": "^4.0.8",
5252
"lucide-react": "^0.475.0",
53-
"next": "15.1.6",
53+
"next": "15.1.7",
5454
"next-auth": "^5.0.0-beta.25",
5555
"next-themes": "^0.4.4",
5656
"react": "19.0.0",
@@ -59,20 +59,20 @@
5959
"react-hook-form": "^7.54.2",
6060
"react-select": "^5.10.0",
6161
"resend": "^4.1.2",
62-
"simple-icons": "^14.5.0",
62+
"simple-icons": "^14.6.0",
6363
"sonner": "^1.7.4",
6464
"tailwind-merge": "^3.0.1",
6565
"tailwindcss-animate": "^1.0.7",
6666
"usehooks-ts": "^3.1.1",
6767
"vaul": "^1.1.2",
6868
"web-push": "^3.6.7",
69-
"zod": "^3.24.1"
69+
"zod": "^3.24.2"
7070
},
7171
"devDependencies": {
72-
"eslint": "^9.20.0",
73-
"eslint-config-next": "15.1.6",
74-
"postcss": "^8.5.1",
72+
"eslint": "^9.20.1",
73+
"eslint-config-next": "15.1.7",
74+
"postcss": "^8.5.2",
7575
"prisma": "^6.3.1",
76-
"tailwindcss": "^4.0.4"
76+
"tailwindcss": "^4.0.6"
7777
}
7878
}

scripts/upload.sh

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/bin/sh
2+
3+
# Check if rclone is installed
4+
if ! command -v rclone >/dev/null 2>&1; then
5+
echo "rclone is not installed. Install it first:"
6+
echo "sudo apt install rclone"
7+
exit 1
8+
fi
9+
10+
# Check if rclone is configured for Google Drive
11+
if ! rclone listremotes | grep -q "wapy_gdrive:"; then
12+
echo "Google Drive remote not configured. Run:"
13+
echo "rclone config"
14+
echo "and follow the instructions to set up Google Drive."
15+
exit 1
16+
fi
17+
18+
# Get current timestamp
19+
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
20+
BACKUP_FILE="wapy_dev_backup_${TIMESTAMP}.tar.gz"
21+
22+
# Create tar.gz archive
23+
mkdir -p .tmp
24+
echo "Creating backup archive..."
25+
tar -czf ".tmp/${BACKUP_FILE}" .backup/
26+
27+
# Create remote backup folder if it doesn't exist
28+
rclone mkdir wapy_gdrive:wapy.dev.backup
29+
30+
# Sync the backup folder to Google Drive
31+
echo "Syncing backup to Google Drive..."
32+
rclone copy ".tmp/${BACKUP_FILE}" wapy_gdrive:wapy.dev.backup --progress
33+
# rclone sync .backup wapy_gdrive:wapy.dev.backup --progress
34+
35+
if [ $? -eq 0 ]; then
36+
# Clean up
37+
rm -rf .tmp
38+
echo "Backup successfully synced to Google Drive"
39+
else
40+
# Clean up
41+
rm -rf .tmp
42+
echo "Error syncing to Google Drive"
43+
exit 1
44+
fi
45+
46+
# Clean up old backups (keep only 10 most recent)
47+
echo "Cleaning up old backups..."
48+
BACKUP_FILE_COUNT=20
49+
BACKUP_COUNT=$(rclone ls wapy_gdrive:wapy.dev.backup | wc -l)
50+
if [ "$BACKUP_COUNT" -gt $BACKUP_FILE_COUNT ]; then
51+
# List files sorted by name (which includes timestamp), get the oldest ones
52+
rclone ls wapy_gdrive:wapy.dev.backup | \
53+
sort | \
54+
head -n $(($BACKUP_COUNT - $BACKUP_FILE_COUNT)) | \
55+
while read -r size name; do
56+
echo "Removing old backup: $name"
57+
rclone delete "wapy_gdrive:wapy.dev.backup/$name"
58+
done
59+
fi
60+
61+
echo "Process complete..."

src/app/account/account-container.js

+55
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
UserUpdateCurrency,
4242
UserUpdateNotifications,
4343
UserUpdateName,
44+
UserExportData,
4445
} from './actions';
4546
import { SchemaCategory, SchemaUserNotifications } from './schema';
4647
import { DefaultCategories } from '@/config/categories';
@@ -993,6 +994,59 @@ const PaymentStatusWrapper = ({ user, paddleStatus }) => {
993994

994995
};
995996

997+
const ExportActions = () => {
998+
const [loading, setLoading] = useState(false);
999+
1000+
const handleExport = useCallback(async () => {
1001+
setLoading(true);
1002+
try {
1003+
const data = await UserExportData();
1004+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1005+
const url = window.URL.createObjectURL(blob);
1006+
const link = document.createElement('a');
1007+
link.href = url;
1008+
link.download = `wapy-dev-export-${format(new Date(), 'yyyy-MM-dd')}.json`;
1009+
document.body.appendChild(link);
1010+
link.click();
1011+
document.body.removeChild(link);
1012+
window.URL.revokeObjectURL(url);
1013+
1014+
toast.success('Data exported successfully!');
1015+
} catch (error) {
1016+
toast.error('Failed to export data');
1017+
} finally {
1018+
setLoading(false);
1019+
}
1020+
}, [UserExportData, setLoading]);
1021+
1022+
return (
1023+
<Card>
1024+
<CardHeader>
1025+
<CardTitle>Data Export</CardTitle>
1026+
<CardDescription>
1027+
Download a copy of your subscriptions, categories and settings
1028+
</CardDescription>
1029+
</CardHeader>
1030+
<CardContent className='space-y-4'>
1031+
<Button
1032+
onClick={handleExport}
1033+
variant='outline'
1034+
disabled={loading}
1035+
className='w-full sm:w-auto'
1036+
title='Export your data'
1037+
>
1038+
{loading ? (
1039+
<Icons.spinner className='mr-2 size-4 animate-spin' />
1040+
) : (
1041+
<Icons.download className='mr-2 size-4' />
1042+
)}
1043+
Export Data
1044+
</Button>
1045+
</CardContent>
1046+
</Card>
1047+
);
1048+
};
1049+
9961050
export const AccountSettings = ({ user, paddleStatus }) => {
9971051
return (
9981052
<div className='w-full max-w-4xl space-y-6 text-left'>
@@ -1001,6 +1055,7 @@ export const AccountSettings = ({ user, paddleStatus }) => {
10011055
<DefaultSettings user={user} />
10021056
<NotificationManager user={user} />
10031057
<CategoryManager user={user} />
1058+
<ExportActions />
10041059
</div>
10051060
);
10061061
};

src/app/account/actions.js

+58
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,61 @@ export const UserUpdateName = async (name) => {
214214
}
215215
});
216216
};
217+
218+
export const UserExportData = async () => {
219+
const session = await auth();
220+
if (!session?.user?.id) {
221+
throw new Error('Unauthorized');
222+
}
223+
224+
const user = await prisma.user.findUnique({
225+
where: {
226+
id: session.user.id,
227+
},
228+
select: {
229+
name: true,
230+
timezone: true,
231+
currency: true,
232+
notifications: true,
233+
categories: {
234+
select: {
235+
name: true,
236+
color: true
237+
}
238+
},
239+
subscriptions: {
240+
select: {
241+
name: true,
242+
logo: true,
243+
enabled: true,
244+
price: true,
245+
currency: true,
246+
paymentDate: true,
247+
untilDate: true,
248+
timezone: true,
249+
cycle: true,
250+
url: true,
251+
notes: true,
252+
categories: {
253+
select: {
254+
name: true,
255+
color: true
256+
}
257+
},
258+
notifications: true,
259+
}
260+
}
261+
}
262+
});
263+
264+
return {
265+
user: {
266+
timezone: user.timezone,
267+
currency: user.currency,
268+
notifications: user.notifications,
269+
name: user?.name || '',
270+
},
271+
categories: user.categories,
272+
subscriptions: user.subscriptions,
273+
};
274+
};

src/app/layout.js

+4-16
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@ import './globals.css';
33

44
// Imports
55
import { Inter } from 'next/font/google';
6-
import Link from 'next/link';
76
import { auth } from '@/lib/auth';
87
import { ThemeProvider, SessionProvider } from '@/components/providers';
98
import Footer from '@/components/footer';
109
import { siteConfig } from '@/components/config';
1110
import { Toaster } from '@/components/ui/sonner';
12-
import { Button } from '@/components/ui/button';
1311
import { CookieConsent } from '@/components/cookie-consent';
1412
import Header from '@/components/header';
15-
import { Icons } from '@/components/icons';
1613
import { PushNotificationProvider } from '@/components/providers';
1714
import { HeaderMemberMainNavigation, HeaderMemberIconNavigation } from '@/components/header-member';
15+
import { HeaderVisitorIconNavigation } from '@/components/header-visitor';
1816
import { PushNotificationToggle } from '@/components/notifications/notification-toggle';
1917
import { AddToHomeScreen } from '@/components/add-to-home-screen';
2018

@@ -80,8 +78,8 @@ export const metadata = {
8078
// Viewport
8179
export const viewport = {
8280
themeColor: [
83-
{ media: '(prefers-color-scheme: light)', color: '0 0% 100%' }, // TODO: does this work?
84-
{ media: '(prefers-color-scheme: dark)', color: '240 10% 3.9%' },
81+
{ media: '(prefers-color-scheme: light)', color: '#FFFFFF' }, // TODO: does this work?
82+
{ media: '(prefers-color-scheme: dark)', color: '#09090B' },
8583
],
8684
width: 'device-width',
8785
initialScale: 1,
@@ -92,16 +90,6 @@ export const viewport = {
9290
export default async function RootLayout({ children }) {
9391
const session = await auth();
9492

95-
const iconNavigation = (
96-
<>
97-
<Button variant='ghost' size='icon' title='#' asChild>
98-
<Link href='/#'>
99-
<Icons.signIn className='h-5 w-5' />
100-
</Link>
101-
</Button>
102-
</>
103-
);
104-
10593
return (
10694
<html lang='en' suppressHydrationWarning>
10795
<body className={`${inter.className} antialiased`}>
@@ -111,7 +99,7 @@ export default async function RootLayout({ children }) {
11199
<div className='flex min-h-screen flex-col'>
112100
<Header
113101
mainNavigation={session ? HeaderMemberMainNavigation : undefined}
114-
iconNavigation={session ? HeaderMemberIconNavigation : iconNavigation}
102+
iconNavigation={session ? HeaderMemberIconNavigation : HeaderVisitorIconNavigation}
115103
/>
116104
<main className='flex flex-col h-full grow items-center p-8 md:p-12'>
117105
<div className='container flex flex-col items-center gap-6 text-center grow relative'>

src/app/#/signin-form.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ const SignInOTP = ({email}) => {
4646
const parsedEmail = signInSchema.safeParse({ email: email });
4747

4848
if (parsedCode?.success && parsedEmail?.success) {
49-
const url = await generateOTPLink(parsedCode.data.code, parsedEmail.data.email);
50-
if (url) {
51-
setUrl(url);
49+
const link = await generateOTPLink(parsedCode.data.code, parsedEmail.data.email);
50+
if (link) {
51+
setUrl(link);
5252
} else {
5353
toast.error('This is not a valid code! Please try again.');
5454
}
@@ -57,12 +57,14 @@ const SignInOTP = ({email}) => {
5757
}
5858

5959
setDisabled(false);
60-
}, []);
60+
}, [setUrl]);
6161

6262
useEffect(() => {
6363
if (url) {
64+
const link = url;
6465
setUrl(null);
65-
router.push(url);
66+
router.push(link);
67+
router.refresh();
6668
}
6769
}, [url]);
6870

src/app/manifest.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ export default function manifest() {
77
description: siteConfig.description,
88
start_url: `/`,
99
icons: [
10-
{ src: '/favicon.ico', type: 'image/x-icon', sizes: '16x16 32x32' },
10+
{ src: '/icons/icon-32.png', type: 'image/png', sizes: '32x32' },
11+
{ src: '/icons/icon-96.png', type: 'image/png', sizes: '96x96' },
1112
{ src: '/icons/icon-192.png', type: 'image/png', sizes: '192x192' },
13+
{ src: '/icons/icon-256.png', type: 'image/png', sizes: '256x256' },
1214
{ src: '/icons/icon-512.png', type: 'image/png', sizes: '512x512' },
1315
{ src: '/web-app-manifest-192x192.png', type: 'image/png', sizes: '192x192', purpose: 'maskable' },
1416
{ src: '/web-app-manifest-512x512.png', type: 'image/png', sizes: '512x512', purpose: 'maskable' },

src/components/footer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default function Footer( { author, github } ) {
4444
Menu
4545
</button>
4646
</DropdownMenuTrigger>
47-
<DropdownMenuContent align='end' className='w-56 space-y-2 px-1 py-2'>
47+
<DropdownMenuContent align='end' className='w-52 space-y-2 px-1 py-2'>
4848
<DropdownMenuItem asChild>
4949
<Link href='/#' className='inline-flex items-center gap-1 w-full cursor-pointer text-sm font-medium focus:outline-hidden'>
5050
<Icons.wallet className='size-4' />

0 commit comments

Comments
 (0)