Skip to content

Commit 0a29584

Browse files
authored
Merge pull request #58 from meceware/dev
v1.3.0
2 parents 5e37e7c + a826808 commit 0a29584

File tree

18 files changed

+1718
-679
lines changed

18 files changed

+1718
-679
lines changed

package-lock.json

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

package.json

+19-19
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
"author": "meceware",
1515
"license": "MIT+Commons Clause",
1616
"dependencies": {
17-
"@auth/prisma-adapter": "^2.7.4",
17+
"@auth/prisma-adapter": "^2.8.0",
1818
"@date-fns/tz": "^1.2.0",
19-
"@hookform/resolvers": "^4.1.0",
20-
"@paddle/paddle-js": "^1.3.3",
21-
"@paddle/paddle-node-sdk": "^2.5.0",
22-
"@prisma/client": "^6.3.1",
19+
"@hookform/resolvers": "^4.1.3",
20+
"@paddle/paddle-js": "^1.4.0",
21+
"@paddle/paddle-node-sdk": "^2.5.1",
22+
"@prisma/client": "^6.5.0",
2323
"@radix-ui/react-accordion": "^1.2.3",
2424
"@radix-ui/react-avatar": "^1.1.3",
2525
"@radix-ui/react-checkbox": "^1.1.4",
@@ -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.6",
40+
"@tailwindcss/postcss": "^4.0.14",
4141
"class-variance-authority": "^0.7.1",
4242
"clsx": "^2.1.1",
4343
"cmdk": "^1.0.4",
@@ -49,30 +49,30 @@
4949
"js-cookie": "^3.0.5",
5050
"jsonwebtoken": "^9.0.2",
5151
"lodash.debounce": "^4.0.8",
52-
"lucide-react": "^0.475.0",
53-
"next": "15.1.7",
52+
"lucide-react": "^0.479.0",
53+
"next": "15.2.1",
5454
"next-auth": "^5.0.0-beta.25",
55-
"next-themes": "^0.4.4",
55+
"next-themes": "^0.4.6",
5656
"react": "19.0.0",
57-
"react-day-picker": "^9.5.1",
57+
"react-day-picker": "^9.6.2",
5858
"react-dom": "19.0.0",
5959
"react-hook-form": "^7.54.2",
60-
"react-select": "^5.10.0",
60+
"react-select": "^5.10.1",
6161
"resend": "^4.1.2",
62-
"simple-icons": "^14.6.0",
63-
"sonner": "^1.7.4",
64-
"tailwind-merge": "^3.0.1",
62+
"simple-icons": "^14.10.0",
63+
"sonner": "^2.0.1",
64+
"tailwind-merge": "^3.0.2",
6565
"tailwindcss-animate": "^1.0.7",
6666
"usehooks-ts": "^3.1.1",
6767
"vaul": "^1.1.2",
6868
"web-push": "^3.6.7",
6969
"zod": "^3.24.2"
7070
},
7171
"devDependencies": {
72-
"eslint": "^9.20.1",
73-
"eslint-config-next": "15.1.7",
74-
"postcss": "^8.5.2",
75-
"prisma": "^6.3.1",
76-
"tailwindcss": "^4.0.6"
72+
"eslint": "^9.22.0",
73+
"eslint-config-next": "15.2.2",
74+
"postcss": "^8.5.3",
75+
"prisma": "^6.5.0",
76+
"tailwindcss": "^4.0.14"
7777
}
7878
}

src/app/api/cron/route.js

+1-9
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { formatDistanceToNowStrict, isEqual, addDays, addMonths, isAfter, isPast
77
import { Resend } from 'resend';
88
import { prisma } from '@/lib/prisma';
99
import { SubscriptionGetNextNotificationDate } from '@/components/subscriptions/lib';
10-
import { DefaultCurrencies } from '@/config/currencies';
1110
import { siteConfig } from '@/components/config';
1211
import { paddleGetStatus } from '@/lib/paddle/status';
1312
import { PADDLE_STATUS_MAP, TRIAL_DURATION_MONTHS, paddleIsValid } from '@/lib/paddle/enum';
13+
import { formatPrice } from '@/components/subscriptions/utils';
1414

1515
const sendNotification = async (subscription, title, message, markAsPaidUrl, isPaymentDueNow) => {
1616
return subscription.user.push.map(async push => {
@@ -252,14 +252,6 @@ const UserSubscriptionNotifications = async (resend, rightNow) => {
252252

253253
export async function GET() {
254254
const resend = new Resend(process.env.RESEND_API_KEY);
255-
256-
const formatPrice = (price, curr) => {
257-
const currency = DefaultCurrencies[curr];
258-
return currency.position === 'before'
259-
? `${currency.symbol}${price}`
260-
: `${price}${currency.symbol}`;
261-
};
262-
263255
const rightNow = new Date();
264256

265257
await UserSubscriptionNotifications(resend, rightNow);

src/app/layout.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export default async function RootLayout({ children }) {
173173
mainNavigation={session ? HeaderMemberMainNavigation : undefined}
174174
iconNavigation={session ? HeaderMemberIconNavigation : HeaderVisitorIconNavigation}
175175
/>
176-
<main className='flex flex-col h-full grow items-center p-8 md:p-12'>
176+
<main className='flex flex-col h-full grow items-center p-4 sm:p-8 md:p-12'>
177177
<div className='container flex flex-col items-center gap-6 text-center grow relative'>
178178
{children}
179179
</div>

src/app/#/signin-form.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,22 @@ const SignInOTP = ({email}) => {
5757
}
5858

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

6262
useEffect(() => {
63-
if (url) {
64-
const link = url;
63+
let isMounted = true;
64+
65+
if (url && isMounted) {
66+
const redirectUrl = url;
6567
setUrl(null);
66-
router.push(link);
67-
router.refresh();
68+
69+
router.replace(redirectUrl);
6870
}
69-
}, [url]);
71+
72+
return () => {
73+
isMounted = false;
74+
};
75+
}, [url, router]);
7076

7177
return (
7278
<InputOTP

src/app/privacy/policy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const PrivacyPolicy = () => {
9090
<section className='space-y-4'>
9191
<h2 className='text-2xl font-semibold'>Contact Us</h2>
9292
<p>
93-
If you have any questions about our Privacy Policy or how we handle your data, please contact us using the <Link href='/contact' className='font-medium underline underline-offset-4 focus:outline-hidden'>contact form</Link>.
93+
If you have any questions about our Privacy Policy or how we handle your data, please contact us using the <Link href='/contact' className='font-medium underline underline-offset-4 focus:outline-hidden' title='Contact form'>contact form</Link>.
9494
</p>
9595
</section>
9696

src/app/refund-policy/refund-policy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const RefundPolicy = () => {
1717

1818
<section className='space-y-4'>
1919
<p>
20-
Bad refund policies are infuriating. You feel like the company is just trying to rip you off. We never want our customers to feel that way, so our refund policy is simple: If you&apos;re ever unhappy for any reason, just <Link href='/contact' className='font-medium underline underline-offset-4 focus:outline-hidden'>contact us</Link> and tell us what&apos;s up, and we&apos;ll work with you to make sure you&apos;re happy.
20+
Bad refund policies are infuriating. You feel like the company is just trying to rip you off. We never want our customers to feel that way, so our refund policy is simple: If you&apos;re ever unhappy for any reason, just <Link href='/contact' className='font-medium underline underline-offset-4 focus:outline-hidden' title='Contact form'>contact us</Link> and tell us what&apos;s up, and we&apos;ll work with you to make sure you&apos;re happy.
2121
</p>
2222
</section>
2323

src/app/sitemap.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function sitemap() {
55
{
66
url: `${siteConfig.url}`,
77
lastModified: new Date().toISOString(),
8-
changeFrequency: 'monthly',
8+
changeFrequency: 'weekly',
99
priority: 1
1010
},
1111
{

src/app/view/[slug]/page.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use server';
2+
3+
import { notFound } from 'next/navigation';
4+
import { auth } from '@/lib/auth';
5+
import { withAuth } from '@/lib/with-auth';
6+
import { SubscriptionGet } from '@/components/subscriptions/actions';
7+
import { SubscriptionView } from '@/components/subscriptions/view';
8+
import { paddleGetSession } from '@/lib/paddle/status';
9+
import { SubscriptionGuard } from '@/components/subscription-guard';
10+
11+
const PageSubscriptionView = async ({ params }) => {
12+
const slug = (await params).slug;
13+
const { session, user, paddleStatus } = await paddleGetSession();
14+
15+
const subscription = await SubscriptionGet(slug, session?.user?.id);
16+
if (!subscription) {
17+
return notFound();
18+
}
19+
20+
return (
21+
<SubscriptionGuard paddleStatus={paddleStatus}>
22+
<div className='container flex flex-col items-center justify-center gap-6'>
23+
<SubscriptionView user={user} subscription={subscription} />
24+
</div>
25+
</SubscriptionGuard>
26+
);
27+
};
28+
29+
export default withAuth(PageSubscriptionView);
30+
31+
export async function generateMetadata({ params }) {
32+
const session = await auth();
33+
if (!session?.user?.id) {
34+
return {
35+
title: 'Unauthorized',
36+
};
37+
}
38+
39+
const subscriptionId = (await params).slug;
40+
const subscription = await SubscriptionGet(subscriptionId, session?.user?.id);
41+
return {
42+
title: subscription?.name ? `View ${subscription.name}` : 'Not Found',
43+
};
44+
}

src/app/view/page.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { notFound } from 'next/navigation';
2+
3+
export default function PageView() {
4+
return notFound();
5+
}
6+
7+
export const metadata = {
8+
title: 'Page Not Found',
9+
};

src/components/home-visitor.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ export const HomeVisitor = () => {
1111
{/* Hero Section */}
1212
<div className='flex flex-col items-center text-center gap-6'>
1313
<div className='inline-block px-4 py-1.5 bg-primary/10 text-primary rounded-full text-sm font-medium mb-4'>
14-
Manage your subscriptions smarter
14+
Want to control your recurring expenses?
1515
</div>
1616
<h1 className='text-3xl md:text-6xl font-bold tracking-tight'>
17-
Take Control with
18-
<br />
19-
Best Subscription Tracker
17+
Smart Subscription Management Made Easy
2018
</h1>
2119
<h2 className='text-xl text-muted-foreground-light max-w-4xl'>
2220
{siteConfig.name} helps you track subscriptions, monitor recurring expenses, and get payment reminders in one powerful and human readable dashboard.

src/components/subscriptions/card.js

+11-52
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
PopoverContent,
2626
PopoverTrigger,
2727
} from '@/components/ui/popover';
28+
import { getCycleLabel, getPaymentCount, formatPrice } from '@/components/subscriptions/utils';
2829

2930
const SubscriptionDate = ({date, timezone, text}) => {
3031
return (
@@ -87,7 +88,7 @@ const SubscriptionMarkAsPaid = ({ subscription }) => {
8788
<div>
8889
<span className='text-sm text-muted-foreground'>Did you pay this?</span>
8990
{' '}
90-
<Button variant='link' className='underline p-0 h-auto' onClick={markAsPaid}>
91+
<Button variant='link' className='underline p-0 h-auto cursor-pointer' onClick={markAsPaid}>
9192
Mark as paid
9293
</Button>
9394
</div>
@@ -126,35 +127,11 @@ const SubscriptionPaymentCount = ({ subscription }) => {
126127
return null;
127128
}
128129

129-
const getPaymentCount = () => {
130-
const startDate = toZonedTime(subscription.paymentDate, subscription.timezone);
131-
const endDate = toZonedTime(subscription.untilDate, subscription.timezone);
132-
const cycle = subscription.cycle;
133-
134-
if (endDate < startDate) {
135-
return 0;
136-
}
137-
138-
if (cycle.time === 'DAYS') {
139-
return Math.floor(DateFNS.differenceInDays(endDate, startDate) / cycle.every) + 1;
140-
}
141-
142-
if (cycle.time === 'WEEKS') {
143-
return Math.floor(DateFNS.differenceInWeeks(endDate, startDate) / cycle.every) + 1;
144-
}
145-
146-
if (cycle.time === 'MONTHS') {
147-
return Math.floor(DateFNS.differenceInMonths(endDate, startDate) / cycle.every) + 1;
148-
}
149-
150-
if (cycle.time === 'YEARS') {
151-
return Math.floor(DateFNS.differenceInYears(endDate, startDate) / cycle.every) + 1;
152-
}
153-
154-
return 0;
155-
};
156-
157-
const paymentCount = getPaymentCount();
130+
const paymentCount = getPaymentCount(
131+
toZonedTime(subscription.paymentDate, subscription.timezone),
132+
toZonedTime(subscription.untilDate, subscription.timezone),
133+
subscription.cycle
134+
);
158135

159136
if (paymentCount === 0) {
160137
return (
@@ -189,43 +166,26 @@ export const SubscriptionCard = ({ subscription }) => {
189166
const parsedIcon = subscription.logo ? JSON.parse(subscription.logo) : false;
190167
const currency = DefaultCurrencies[subscription.currency];
191168
const categories = subscription.categories || [];
192-
const cycle = subscription.cycle;
193169
const isPushEnabled = subscription.enabled && subscription.notifications.some(notification => notification.type.includes('PUSH'));
194170
const isEmailEnabled = subscription.enabled && subscription.notifications.some(notification => notification.type.includes('EMAIL'));
195171

196-
const formatPrice = (price) => {
197-
return currency.position === 'before'
198-
? `${currency.symbol}${price}`
199-
: `${price}${currency.symbol}`;
200-
};
201-
202-
const getCycleLabel = () => {
203-
if (cycle.every === 1) {
204-
if (cycle.time === 'DAYS') return 'Daily';
205-
if (cycle.time === 'WEEKS') return 'Weekly';
206-
if (cycle.time === 'MONTHS') return 'Monthly';
207-
if (cycle.time === 'YEARS') return 'Annually';
208-
}
209-
return `Every ${cycle.every} ${cycle.time.toLowerCase()}`;
210-
};
211-
212172
return (
213173
<Card className='w-full hover:shadow-lg transition-shadow duration-200 flex flex-col'>
214174
<CardHeader className='pt-4'>
215175
<div className='flex items-start justify-between gap-2'>
216176
<div className='flex flex-col gap-1 text-left grow overflow-hidden'>
217177
<div className='inline-flex items-center gap-2'>
218-
<CardTitle className='text-2xl truncate'>{subscription.name}</CardTitle>
178+
<CardTitle className='text-2xl truncate'><Link href={`/view/${subscription.id}`}>{subscription.name}</Link></CardTitle>
219179
</div>
220180
<div className='w-full text-sm text-muted-foreground truncate'>
221-
<span className='font-medium text-lg text-foreground'>{formatPrice(subscription.price)}</span>
181+
<span className='font-medium text-lg text-foreground'>{formatPrice(subscription.price, subscription.currency)}</span>
222182
<span className='ml-1'>
223-
/ {getCycleLabel()}
183+
/ {getCycleLabel(subscription.cycle)}
224184
</span>
225185
</div>
226186
</div>
227187
<div className='relative shrink-0 size-16 rounded-full flex items-center justify-center bg-gray-200 dark:bg-gray-800'>
228-
<div className={cn('size-4 rounded-full absolute top-0 right-0', {'bg-green-600': subscription.enabled}, {'bg-red-600': !subscription.enabled})}>
188+
<div className={cn('size-4 rounded-full absolute top-0 right-0 ring-2 ring-background', {'bg-green-600': subscription.enabled}, {'bg-red-600': !subscription.enabled})}>
229189
</div>
230190
<LogoIcon icon={parsedIcon}>
231191
<span className='text-2xl'>{subscription.name[0].toUpperCase()}</span>
@@ -265,7 +225,6 @@ export const SubscriptionCard = ({ subscription }) => {
265225
style={{
266226
color: category.color,
267227
borderColor: category.color,
268-
fontSize: '0.75rem'
269228
}}
270229
>
271230
{category.name}

0 commit comments

Comments
 (0)