Skip to content

Commit 1c86d75

Browse files
Convert to TypeScript. (vercel#87)
1 parent 00ab2d4 commit 1c86d75

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1317
-3176
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ yarn-error.log*
3232

3333
# vercel
3434
.vercel
35+
36+
# editors
37+
.vscode

components/Layout.js renamed to components/Layout.tsx

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import Head from 'next/head';
22
import { useRouter } from 'next/router';
33

4-
import Navbar from '@/components/ui/Navbar';
5-
import Footer from '@/components/ui/Footer';
4+
import Navbar from 'components/ui/Navbar';
5+
import Footer from 'components/ui/Footer';
6+
import { ReactNode } from 'react';
7+
import { PageMeta } from '../types';
68

7-
export default function Layout({ children, meta: pageMeta }) {
9+
interface Props {
10+
children: ReactNode;
11+
meta?: PageMeta;
12+
}
13+
14+
export default function Layout({ children, meta: pageMeta }: Props) {
815
const router = useRouter();
916
const meta = {
1017
title: 'Next.js Subscription Starter',
@@ -20,10 +27,7 @@ export default function Layout({ children, meta: pageMeta }) {
2027
<meta name="robots" content="follow, index" />
2128
<link href="/favicon.ico" rel="shortcut icon" />
2229
<meta content={meta.description} name="description" />
23-
<meta
24-
property="og:url"
25-
content={`https://subscription-starter.vercel.app${router.asPath}`}
26-
/>
30+
<meta property="og:url" content={`https://subscription-starter.vercel.app${router.asPath}`} />
2731
<meta property="og:type" content="website" />
2832
<meta property="og:site_name" content={meta.title} />
2933
<meta property="og:description" content={meta.description} />

components/Pricing.js renamed to components/Pricing.tsx

+26-16
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@ import cn from 'classnames';
22
import { useRouter } from 'next/router';
33
import { useState } from 'react';
44

5-
import Button from '@/components/ui/Button';
6-
import { postData } from '@/utils/helpers';
7-
import { getStripe } from '@/utils/stripe-client';
8-
import { useUser } from '@/utils/useUser';
5+
import Button from 'components/ui/Button';
6+
import { postData } from 'utils/helpers';
7+
import { getStripe } from 'utils/stripe-client';
8+
import { useUser } from 'utils/useUser';
9+
import { Product, Price, ProductWithPrice } from 'types';
910

10-
export default function Pricing({ products }) {
11+
interface Props {
12+
products: ProductWithPrice[];
13+
}
14+
type BillingInterval = 'year' | 'month';
15+
16+
export default function Pricing({ products }: Props) {
1117
const router = useRouter();
12-
const [billingInterval, setBillingInterval] = useState('month');
13-
const [priceIdLoading, setPriceIdLoading] = useState();
18+
const [billingInterval, setBillingInterval] = useState<BillingInterval>(
19+
'month'
20+
);
21+
const [priceIdLoading, setPriceIdLoading] = useState<string>();
1422
const { session, userLoaded, subscription } = useUser();
1523

16-
const handleCheckout = async (price) => {
24+
const handleCheckout = async (price: Price) => {
1725
setPriceIdLoading(price.id);
1826
if (!session) {
1927
return router.push('/signin');
@@ -30,11 +38,11 @@ export default function Pricing({ products }) {
3038
});
3139

3240
const stripe = await getStripe();
33-
stripe.redirectToCheckout({ sessionId });
41+
stripe?.redirectToCheckout({ sessionId });
3442
} catch (error) {
35-
return alert(error.message);
43+
return alert((error as Error)?.message);
3644
} finally {
37-
setPriceIdLoading(false);
45+
setPriceIdLoading(undefined);
3846
}
3947
};
4048

@@ -97,22 +105,24 @@ export default function Pricing({ products }) {
97105
</div>
98106
<div className="mt-12 space-y-4 sm:mt-16 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6 lg:max-w-4xl lg:mx-auto xl:max-w-none xl:mx-0 xl:grid-cols-4">
99107
{products.map((product) => {
100-
const price = product.prices.find(
108+
const price = product?.prices?.find(
101109
(price) => price.interval === billingInterval
102110
);
111+
if (!price) return null;
103112
const priceString = new Intl.NumberFormat('en-US', {
104113
style: 'currency',
105114
currency: price.currency,
106115
minimumFractionDigits: 0
107-
}).format(price.unit_amount / 100);
116+
}).format((price?.unit_amount || 0) / 100);
108117
return (
109118
<div
110119
key={product.id}
111120
className={cn(
112121
'rounded-lg shadow-sm divide-y divide-accents-2 bg-primary-2',
113122
{
114123
'border border-pink': subscription
115-
? product.name === subscription?.prices?.products.name
124+
? product.name ===
125+
subscription?.prices?.products?.[0]?.name
116126
: product.name === 'Freelancer'
117127
}
118128
)}
@@ -135,10 +145,10 @@ export default function Pricing({ products }) {
135145
type="button"
136146
disabled={session && !userLoaded}
137147
loading={priceIdLoading === price.id}
138-
onClick={() => handleCheckout(price.id)}
148+
onClick={() => handleCheckout(price)}
139149
className="mt-8 block w-full rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
140150
>
141-
{product.name === subscription?.prices?.products.name
151+
{product.name === subscription?.prices?.products?.[0]?.name
142152
? 'Manage'
143153
: 'Subscribe'}
144154
</Button>
File renamed without changes.

components/icons/Logo.js renamed to components/icons/Logo.tsx

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
const Logo = ({ className = '', ...props }) => (
2-
<svg
3-
width="32"
4-
height="32"
5-
viewBox="0 0 32 32"
6-
fill="none"
7-
xmlns="http://www.w3.org/2000/svg"
8-
className={className}
9-
{...props}
10-
>
2+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...props}>
113
<rect width="100%" height="100%" rx="16" fill="var(--secondary)" />
124
<path
135
fillRule="evenodd"

components/ui/Button/Button.js renamed to components/ui/Button/Button.tsx

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import cn from 'classnames';
2-
import React, { forwardRef, useRef } from 'react';
2+
import React, { forwardRef, useRef, ButtonHTMLAttributes } from 'react';
33
import mergeRefs from 'react-merge-refs';
4-
import s from './Button.module.css';
4+
import styles from './Button.module.css';
55

6-
import LoadingDots from '@/components/ui/LoadingDots';
6+
import LoadingDots from 'components/ui/LoadingDots';
77

8-
const Button = forwardRef((props, buttonRef) => {
8+
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
9+
variant?: 'slim' | 'flat';
10+
active?: boolean;
11+
width?: number;
12+
loading?: boolean;
13+
Component?: React.ComponentType;
14+
}
15+
16+
const Button = forwardRef<HTMLButtonElement, Props>((props, buttonRef) => {
917
const {
1018
className,
1119
variant = 'flat',
@@ -20,15 +28,14 @@ const Button = forwardRef((props, buttonRef) => {
2028
} = props;
2129
const ref = useRef(null);
2230
const rootClassName = cn(
23-
s.root,
31+
styles.root,
2432
{
25-
[s.slim]: variant === 'slim',
26-
[s.loading]: loading,
27-
[s.disabled]: disabled
33+
[styles.slim]: variant === 'slim',
34+
[styles.loading]: loading,
35+
[styles.disabled]: disabled
2836
},
2937
className
3038
);
31-
3239
return (
3340
<Component
3441
aria-pressed={active}
File renamed without changes.

components/ui/Footer/Footer.js renamed to components/ui/Footer/Footer.tsx

+11-33
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Link from 'next/link';
22
import s from './Footer.module.css';
33

4-
import Logo from '@/components/icons/Logo';
5-
import GitHub from '@/components/icons/GitHub';
4+
import Logo from 'components/icons/Logo';
5+
import GitHub from 'components/icons/GitHub';
66

77
export default function Footer() {
88
return (
@@ -22,64 +22,46 @@ export default function Footer() {
2222
<ul className="flex flex-initial flex-col md:flex-1">
2323
<li className="py-3 md:py-0 md:pb-4">
2424
<Link href="/">
25-
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
26-
Home
27-
</a>
25+
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">Home</a>
2826
</Link>
2927
</li>
3028
<li className="py-3 md:py-0 md:pb-4">
3129
<Link href="/">
32-
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
33-
About
34-
</a>
30+
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">About</a>
3531
</Link>
3632
</li>
3733
<li className="py-3 md:py-0 md:pb-4">
3834
<Link href="/">
39-
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
40-
Careers
41-
</a>
35+
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">Careers</a>
4236
</Link>
4337
</li>
4438
<li className="py-3 md:py-0 md:pb-4">
4539
<Link href="/">
46-
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
47-
Blog
48-
</a>
40+
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">Blog</a>
4941
</Link>
5042
</li>
5143
</ul>
5244
</div>
5345
<div className="col-span-1 lg:col-span-2">
5446
<ul className="flex flex-initial flex-col md:flex-1">
5547
<li className="py-3 md:py-0 md:pb-4">
56-
<p className="text-primary font-bold hover:text-accents-6 transition ease-in-out duration-150">
57-
LEGAL
58-
</p>
48+
<p className="text-primary font-bold hover:text-accents-6 transition ease-in-out duration-150">LEGAL</p>
5949
</li>
6050
<li className="py-3 md:py-0 md:pb-4">
6151
<Link href="/">
62-
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
63-
Privacy Policy
64-
</a>
52+
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">Privacy Policy</a>
6553
</Link>
6654
</li>
6755
<li className="py-3 md:py-0 md:pb-4">
6856
<Link href="/">
69-
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
70-
Terms of Use
71-
</a>
57+
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">Terms of Use</a>
7258
</Link>
7359
</li>
7460
</ul>
7561
</div>
7662
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
7763
<div className="flex space-x-6 items-center h-10">
78-
<a
79-
aria-label="Github Repository"
80-
href="https://github.com/vercel/nextjs-subscription-payments"
81-
className={s.link}
82-
>
64+
<a aria-label="Github Repository" href="https://github.com/vercel/nextjs-subscription-payments" className={s.link}>
8365
<GitHub />
8466
</a>
8567
</div>
@@ -92,11 +74,7 @@ export default function Footer() {
9274
<div className="flex items-center">
9375
<span className="text-primary">Crafted by</span>
9476
<a href="https://vercel.com" aria-label="Vercel.com Link">
95-
<img
96-
src="/vercel.svg"
97-
alt="Vercel.com Logo"
98-
className="inline-block h-6 ml-4 text-primary"
99-
/>
77+
<img src="/vercel.svg" alt="Vercel.com Logo" className="inline-block h-6 ml-4 text-primary" />
10078
</a>
10179
</div>
10280
</div>
File renamed without changes.

components/ui/Input/Input.js renamed to components/ui/Input/Input.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import React from 'react';
1+
import React, { InputHTMLAttributes, ChangeEvent } from 'react';
22
import cn from 'classnames';
33
import s from './Input.module.css';
44

5-
const Input = (props) => {
5+
interface Props extends Omit<InputHTMLAttributes<any>, 'onChange'> {
6+
className?: string;
7+
onChange: (value: string) => void;
8+
}
9+
const Input = (props: Props) => {
610
const { className, children, onChange, ...rest } = props;
711

812
const rootClassName = cn(s.root, {}, className);
913

10-
const handleOnChange = (e) => {
14+
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
1115
if (onChange) {
1216
onChange(e.target.value);
1317
}
File renamed without changes.

components/ui/Navbar/Navbar.js renamed to components/ui/Navbar/Navbar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Link from 'next/link';
22
import s from './Navbar.module.css';
33

4-
import Logo from '@/components/icons/Logo';
5-
import { useUser } from '@/utils/useUser';
4+
import Logo from 'components/icons/Logo';
5+
import { useUser } from 'utils/useUser';
66

77
const Navbar = () => {
88
const { user, signOut } = useUser();
File renamed without changes.

next-env.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/types/global" />
3+
/// <reference types="next/image-types/global" />
4+
5+
// NOTE: This file should not be edited
6+
// see https://nextjs.org/docs/basic-features/typescript for more information.

package.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"@stripe/stripe-js": "1.11.0",
1313
"@supabase/supabase-js": "1.3.2",
1414
"classnames": "2.2.6",
15-
"next": "10.0.5",
15+
"next": "^12.0.3",
1616
"react": "17.0.1",
1717
"react-dom": "17.0.1",
1818
"react-merge-refs": "1.1.0",
@@ -21,15 +21,19 @@
2121
"tailwindcss": "2.0.2"
2222
},
2323
"devDependencies": {
24+
"@types/classnames": "2.2.6",
25+
"@types/node": "^16.10.9",
26+
"@types/react": "^17.0.29",
2427
"postcss": "8.2.4",
2528
"postcss-flexbugs-fixes": "5.0.2",
2629
"postcss-preset-env": "6.7.0",
27-
"prettier": "2.2.1"
30+
"prettier": "2.2.1",
31+
"typescript": "^4.4.4"
2832
},
2933
"prettier": {
3034
"arrowParens": "always",
3135
"singleQuote": true,
3236
"tabWidth": 2,
3337
"trailingComma": "none"
3438
}
35-
}
39+
}

pages/_app.js renamed to pages/_app.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useEffect } from 'react';
2-
import '@/assets/main.css';
3-
import '@/assets/chrome-bug.css';
2+
import 'assets/main.css';
3+
import 'assets/chrome-bug.css';
4+
import React from 'react';
45

5-
import Layout from '@/components/Layout';
6-
import { UserContextProvider } from '@/utils/useUser';
6+
import Layout from 'components/Layout';
7+
import { UserContextProvider } from 'utils/useUser';
8+
import { AppProps } from 'next/app';
79

8-
export default function MyApp({ Component, pageProps }) {
10+
export default function MyApp({ Component, pageProps }: AppProps) {
911
useEffect(() => {
1012
document.body.classList?.remove('loading');
1113
}, []);
File renamed without changes.

0 commit comments

Comments
 (0)