chore(examples): use default prettier for examples/templates (#60530)

## Description
This PR ensures that the default prettier config is used for examples
and templates.

This config is compatible with `prettier@3` as well (upgrading prettier
is bigger change that can be a future PR).

## Changes
- Updated `.prettierrc.json` in root with `"trailingComma": "es5"` (will
be needed upgrading to prettier@3)
- Added `examples/.prettierrc.json` with default config (this will
change every example)
- Added `packages/create-next-app/templates/.prettierrc.json` with
default config (this will change every template)

## Related

- Fixes #54402
- Closes #54409
This commit is contained in:
Steven 2024-01-11 18:01:44 -05:00 committed by GitHub
parent 98b99e408b
commit 4466ba436b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2559 changed files with 21384 additions and 21309 deletions

View file

@ -1,4 +1,5 @@
{
"trailingComma": "es5",
"singleQuote": true,
"semi": false
}

View file

@ -0,0 +1,5 @@
{
"trailingComma": "all",
"singleQuote": false,
"semi": true
}

View file

@ -1,11 +1,11 @@
import { useRouter } from 'next/router'
import Link, { LinkProps } from 'next/link'
import React, { PropsWithChildren, useState, useEffect } from 'react'
import { useRouter } from "next/router";
import Link, { LinkProps } from "next/link";
import React, { PropsWithChildren, useState, useEffect } from "react";
type ActiveLinkProps = LinkProps & {
className?: string
activeClassName: string
}
className?: string;
activeClassName: string;
};
const ActiveLink = ({
children,
@ -13,8 +13,8 @@ const ActiveLink = ({
className,
...props
}: PropsWithChildren<ActiveLinkProps>) => {
const { asPath, isReady } = useRouter()
const [computedClassName, setComputedClassName] = useState(className)
const { asPath, isReady } = useRouter();
const [computedClassName, setComputedClassName] = useState(className);
useEffect(() => {
// Check if the router fields are updated client-side
@ -23,19 +23,19 @@ const ActiveLink = ({
// Static route will be matched via props.href
const linkPathname = new URL(
(props.as || props.href) as string,
location.href
).pathname
location.href,
).pathname;
// Using URL().pathname to get rid of query and hash
const activePathname = new URL(asPath, location.href).pathname
const activePathname = new URL(asPath, location.href).pathname;
const newClassName =
linkPathname === activePathname
? `${className} ${activeClassName}`.trim()
: className
: className;
if (newClassName !== computedClassName) {
setComputedClassName(newClassName)
setComputedClassName(newClassName);
}
}
}, [
@ -46,13 +46,13 @@ const ActiveLink = ({
activeClassName,
className,
computedClassName,
])
]);
return (
<Link className={computedClassName} {...props}>
{children}
</Link>
)
}
);
};
export default ActiveLink
export default ActiveLink;

View file

@ -1,4 +1,4 @@
import ActiveLink from './ActiveLink'
import ActiveLink from "./ActiveLink";
const Nav = () => (
<nav>
@ -8,7 +8,7 @@ const Nav = () => (
}
.active:after {
content: ' (current page)';
content: " (current page)";
}
`}</style>
<ul className="nav">
@ -39,6 +39,6 @@ const Nav = () => (
</li>
</ul>
</nav>
)
);
export default Nav
export default Nav;

View file

@ -3,9 +3,9 @@ module.exports = {
async rewrites() {
return [
{
source: '/blog',
destination: '/news',
source: "/blog",
destination: "/news",
},
]
];
},
}
};

View file

@ -1,14 +1,14 @@
import { useRouter } from 'next/router'
import Nav from '../components/Nav'
import { useRouter } from "next/router";
import Nav from "../components/Nav";
const SlugPage = () => {
const { asPath } = useRouter()
const { asPath } = useRouter();
return (
<>
<Nav />
<p>Hello, I'm the {asPath} page</p>
</>
)
}
);
};
export default SlugPage
export default SlugPage;

View file

@ -1,10 +1,10 @@
import Nav from '../components/Nav'
import Nav from "../components/Nav";
const AboutPage = () => (
<>
<Nav />
<p>Hello, I'm the about page</p>
</>
)
);
export default AboutPage
export default AboutPage;

View file

@ -1,10 +1,10 @@
import Nav from '../components/Nav'
import Nav from "../components/Nav";
const IndexPage = () => (
<>
<Nav />
<p>Hello, I'm the index page</p>
</>
)
);
export default IndexPage
export default IndexPage;

View file

@ -1,10 +1,10 @@
import Nav from '../components/Nav'
import Nav from "../components/Nav";
const News = () => (
<>
<Nav />
<p>Hello, I'm the news page</p>
</>
)
);
export default News
export default News;

View file

@ -3,16 +3,16 @@
// Only the intrinsic elements defined here will be accepted, and only with the attributes defined here
declare namespace JSX {
interface AmpImg {
alt?: string
src?: string
width?: string | number
height?: string | number
layout?: string
fallback?: string
children?: React.ReactNode
alt?: string;
src?: string;
width?: string | number;
height?: string | number;
layout?: string;
fallback?: string;
children?: React.ReactNode;
}
interface IntrinsicElements {
'amp-img': AmpImg
"amp-img": AmpImg;
}
}

View file

@ -1,6 +1,6 @@
type BylineProps = {
author: string
}
author: string;
};
export default function Byline({ author }: BylineProps) {
return (
@ -13,5 +13,5 @@ export default function Byline({ author }: BylineProps) {
}
`}</style>
</>
)
);
}

View file

@ -1,6 +1,6 @@
type LayoutProps = {
children?: React.ReactNode
}
children?: React.ReactNode;
};
export default function Layout({ children }: LayoutProps) {
return (
@ -14,5 +14,5 @@ export default function Layout({ children }: LayoutProps) {
}
`}</style>
</>
)
);
}

View file

@ -1,13 +1,13 @@
import Head from 'next/head'
import { useAmp } from 'next/amp'
import Byline from '../components/Byline'
import Head from "next/head";
import { useAmp } from "next/amp";
import Byline from "../components/Byline";
export const config = {
amp: 'hybrid',
}
amp: "hybrid",
};
export default function DogPage() {
const isAmp = useAmp()
const isAmp = useAmp();
return (
<div>
@ -17,14 +17,14 @@ export default function DogPage() {
<h1>The Dog (Hybrid AMP Page)</h1>
<Byline author="Meow Meow Fuzzyface" />
<p>
<a href={isAmp ? '/dog' : '/dog?amp=1'}>
{isAmp ? 'View Non-AMP' : 'View AMP'} Version
<a href={isAmp ? "/dog" : "/dog?amp=1"}>
{isAmp ? "View Non-AMP" : "View AMP"} Version
</a>
</p>
<p className="caption">Woooooooooooof</p>
<p>
Wafer donut candy soufflé{' '}
<a href={isAmp ? '/?amp=1' : '/'}>lemon drops</a> icing. Marzipan gummi
Wafer donut candy soufflé{" "}
<a href={isAmp ? "/?amp=1" : "/"}>lemon drops</a> icing. Marzipan gummi
bears pie danish lollipop pudding powder gummi bears sweet. Pie sweet
roll sweet roll topping chocolate bar dragée pudding chocolate cake.
Croissant sweet chocolate bar cheesecake candy canes. Tootsie roll icing
@ -90,5 +90,5 @@ export default function DogPage() {
tiramisu.
</p>
</div>
)
);
}

View file

@ -1,14 +1,14 @@
import Head from 'next/head'
import { useAmp } from 'next/amp'
import Layout from '../components/Layout'
import Byline from '../components/Byline'
import Head from "next/head";
import { useAmp } from "next/amp";
import Layout from "../components/Layout";
import Byline from "../components/Byline";
export const config = {
amp: true,
}
};
export default function IndexPage() {
const isAmp = useAmp()
const isAmp = useAmp();
return (
<Layout>
@ -35,7 +35,7 @@ export default function IndexPage() {
></amp-img>
</amp-img>
<p>
Cat ipsum dolor <a href={isAmp ? '/dog?amp=1' : '/dog'}>sit amet</a>,
Cat ipsum dolor <a href={isAmp ? "/dog?amp=1" : "/dog"}>sit amet</a>,
eat grass, throw it back up but refuse to leave cardboard box or groom
yourself 4 hours - checked, have your beauty sleep 18 hours - checked,
be fabulous for the rest of the day - checked!. Hide from vacuum
@ -233,5 +233,5 @@ export default function IndexPage() {
}
`}</style>
</Layout>
)
);
}

View file

@ -1,3 +1,3 @@
export default function NormalPage() {
return <p>I'm just a normal old page, no AMP for me</p>
return <p>I'm just a normal old page, no AMP for me</p>;
}

View file

@ -1,10 +1,10 @@
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// any configs you need
}
};
module.exports = withBundleAnalyzer(nextConfig)
module.exports = withBundleAnalyzer(nextConfig);

View file

@ -1,5 +1,5 @@
const About = () => {
return <div>About us</div>
}
return <div>About us</div>;
};
export default About
export default About;

View file

@ -1,5 +1,5 @@
const Contact = () => {
return <div>This is the contact page.</div>
}
return <div>This is the contact page.</div>;
};
export default Contact
export default Contact;

View file

@ -1,10 +1,10 @@
import { NextPage, GetStaticProps } from 'next'
import Link from 'next/link'
import { faker } from '@faker-js/faker'
import { NextPage, GetStaticProps } from "next";
import Link from "next/link";
import { faker } from "@faker-js/faker";
type IndexProps = {
name: string
}
name: string;
};
const Index: NextPage<IndexProps> = ({ name }) => {
return (
@ -15,16 +15,16 @@ const Index: NextPage<IndexProps> = ({ name }) => {
<Link href="/about">About Page</Link>
</div>
</div>
)
}
);
};
export default Index
export default Index;
export const getStaticProps: GetStaticProps = async () => {
// The name will be generated at build time only
const name = faker.name.findName()
const name = faker.name.findName();
return {
props: { name },
}
}
};
};

View file

@ -1,54 +1,54 @@
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
import { SchemaLink } from '@apollo/client/link/schema'
import { schema } from '../apollo/schema'
import merge from 'deepmerge'
import { useMemo } from "react";
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { SchemaLink } from "@apollo/client/link/schema";
import { schema } from "../apollo/schema";
import merge from "deepmerge";
let apolloClient
let apolloClient;
function createIsomorphLink() {
if (typeof window === 'undefined') {
return new SchemaLink({ schema })
if (typeof window === "undefined") {
return new SchemaLink({ schema });
} else {
return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
uri: "/api/graphql",
credentials: "same-origin",
});
}
}
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
ssrMode: typeof window === "undefined",
link: createIsomorphLink(),
cache: new InMemoryCache(),
})
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
const _apolloClient = apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
const existingCache = _apolloClient.extract();
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = merge(initialState, existingCache)
const data = merge(initialState, existingCache);
// Restore the cache with the merged data
_apolloClient.cache.restore(data)
_apolloClient.cache.restore(data);
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
if (typeof window === "undefined") return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}

View file

@ -1,53 +1,53 @@
import { createUser, findUser, validatePassword } from '../lib/user'
import { setLoginSession, getLoginSession } from '../lib/auth'
import { removeTokenCookie } from '../lib/auth-cookies'
import { GraphQLError } from 'graphql'
import { createUser, findUser, validatePassword } from "../lib/user";
import { setLoginSession, getLoginSession } from "../lib/auth";
import { removeTokenCookie } from "../lib/auth-cookies";
import { GraphQLError } from "graphql";
export const resolvers = {
Query: {
async viewer(_root, _args, context, _info) {
try {
const session = await getLoginSession(context.req)
const session = await getLoginSession(context.req);
if (session) {
return findUser({ email: session.email })
return findUser({ email: session.email });
}
} catch (error) {
throw new GraphQLError(
'Authentication token is invalid, please log in',
"Authentication token is invalid, please log in",
{
extensions: {
code: 'UNAUTHENTICATED',
code: "UNAUTHENTICATED",
},
}
)
},
);
}
},
},
Mutation: {
async signUp(_parent, args, _context, _info) {
const user = await createUser(args.input)
return { user }
const user = await createUser(args.input);
return { user };
},
async signIn(_parent, args, context, _info) {
const user = await findUser({ email: args.input.email })
const user = await findUser({ email: args.input.email });
if (user && (await validatePassword(user, args.input.password))) {
const session = {
id: user.id,
email: user.email,
}
};
await setLoginSession(context.res, session)
await setLoginSession(context.res, session);
return { user }
return { user };
}
throw new GraphQLError('Invalid email and password combination')
throw new GraphQLError("Invalid email and password combination");
},
async signOut(_parent, _args, context, _info) {
removeTokenCookie(context.res)
return true
removeTokenCookie(context.res);
return true;
},
},
}
};

View file

@ -1,8 +1,8 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import { typeDefs } from './type-defs'
import { resolvers } from './resolvers'
import { makeExecutableSchema } from "@graphql-tools/schema";
import { typeDefs } from "./type-defs";
import { resolvers } from "./resolvers";
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
});

View file

@ -1,4 +1,4 @@
import { gql } from '@apollo/client'
import { gql } from "@apollo/client";
export const typeDefs = gql`
type User {
@ -36,4 +36,4 @@ export const typeDefs = gql`
signIn(input: SignInInput!): SignInPayload!
signOut: Boolean!
}
`
`;

View file

@ -1,17 +1,17 @@
export default function Field({ name, label, type, autoComplete, required }) {
return (
<div>
<label id={[name, 'label'].join('-')} htmlFor={[name, 'input'].join('-')}>
<label id={[name, "label"].join("-")} htmlFor={[name, "input"].join("-")}>
{label} {required ? <span title="Required">*</span> : undefined}
</label>
<br />
<input
autoComplete={autoComplete}
id={[name, 'input'].join('-')}
id={[name, "input"].join("-")}
name={name}
required={required}
type={type}
/>
</div>
)
);
}

View file

@ -1,41 +1,41 @@
import { serialize, parse } from 'cookie'
import { serialize, parse } from "cookie";
const TOKEN_NAME = 'token'
const TOKEN_NAME = "token";
export const MAX_AGE = 60 * 60 * 8 // 8 hours
export const MAX_AGE = 60 * 60 * 8; // 8 hours
export function setTokenCookie(res, token) {
const cookie = serialize(TOKEN_NAME, token, {
maxAge: MAX_AGE,
expires: new Date(Date.now() + MAX_AGE * 1000),
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
})
secure: process.env.NODE_ENV === "production",
path: "/",
sameSite: "lax",
});
res.setHeader('Set-Cookie', cookie)
res.setHeader("Set-Cookie", cookie);
}
export function removeTokenCookie(res) {
const cookie = serialize(TOKEN_NAME, '', {
const cookie = serialize(TOKEN_NAME, "", {
maxAge: -1,
path: '/',
})
path: "/",
});
res.setHeader('Set-Cookie', cookie)
res.setHeader("Set-Cookie", cookie);
}
export function parseCookies(req) {
// For API Routes we don't need to parse the cookies.
if (req.cookies) return req.cookies
if (req.cookies) return req.cookies;
// For pages we do need to parse the cookies.
const cookie = req.headers?.cookie
return parse(cookie || '')
const cookie = req.headers?.cookie;
return parse(cookie || "");
}
export function getTokenCookie(req) {
const cookies = parseCookies(req)
return cookies[TOKEN_NAME]
const cookies = parseCookies(req);
return cookies[TOKEN_NAME];
}

View file

@ -1,29 +1,29 @@
import Iron from '@hapi/iron'
import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies'
import Iron from "@hapi/iron";
import { MAX_AGE, setTokenCookie, getTokenCookie } from "./auth-cookies";
const TOKEN_SECRET = process.env.TOKEN_SECRET
const TOKEN_SECRET = process.env.TOKEN_SECRET;
export async function setLoginSession(res, session) {
const createdAt = Date.now()
const createdAt = Date.now();
// Create a session object with a max age that we can validate later
const obj = { ...session, createdAt, maxAge: MAX_AGE }
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults)
const obj = { ...session, createdAt, maxAge: MAX_AGE };
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
setTokenCookie(res, token)
setTokenCookie(res, token);
}
export async function getLoginSession(req) {
const token = getTokenCookie(req)
const token = getTokenCookie(req);
if (!token) return
if (!token) return;
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults)
const expiresAt = session.createdAt + session.maxAge * 1000
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
const expiresAt = session.createdAt + session.maxAge * 1000;
// Validate the expiration date of the session
if (Date.now() > expiresAt) {
throw new Error('Session expired')
throw new Error("Session expired");
}
return session
return session;
}

View file

@ -3,11 +3,11 @@ export function getErrorMessage(error) {
for (const graphQLError of error.graphQLErrors) {
if (
graphQLError.extensions &&
graphQLError.extensions.code === 'BAD_USER_INPUT'
graphQLError.extensions.code === "BAD_USER_INPUT"
) {
return graphQLError.message
return graphQLError.message;
}
}
}
return error.message
return error.message;
}

View file

@ -1,46 +1,46 @@
import crypto from 'crypto'
import { v4 as uuidv4 } from 'uuid'
import crypto from "crypto";
import { v4 as uuidv4 } from "uuid";
/**
* User methods. The example doesn't contain a DB, but for real applications you must use a
* db here, such as MongoDB, Fauna, SQL, etc.
*/
const users = []
const users = [];
export async function createUser({ email, password }) {
// Here you should create the user and save the salt and hashed password (some dbs may have
// authentication methods that will do it for you so you don't have to worry about it):
const salt = crypto.randomBytes(16).toString('hex')
const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto
.pbkdf2Sync(password, salt, 1000, 64, 'sha512')
.toString('hex')
.pbkdf2Sync(password, salt, 1000, 64, "sha512")
.toString("hex");
const user = {
id: uuidv4(),
createdAt: Date.now(),
email,
hash,
salt,
}
};
// This is an in memory store for users, there is no data persistence without a proper DB
users.push(user)
users.push(user);
return user
return user;
}
// Here you should lookup for the user in your DB
export async function findUser({ email }) {
// This is an in memory store for users, there is no data persistence without a proper DB
return users.find((user) => user.email === email)
return users.find((user) => user.email === email);
}
// Compare the password of an already fetched user (using `findUser`) and compare the
// password for a potential match
export async function validatePassword(user, inputPassword) {
const inputHash = crypto
.pbkdf2Sync(inputPassword, user.salt, 1000, 64, 'sha512')
.toString('hex')
const passwordsMatch = user.hash === inputHash
return passwordsMatch
.pbkdf2Sync(inputPassword, user.salt, 1000, 64, "sha512")
.toString("hex");
const passwordsMatch = user.hash === inputHash;
return passwordsMatch;
}

View file

@ -1,12 +1,12 @@
import { ApolloProvider } from '@apollo/client'
import { useApollo } from '../apollo/client'
import { ApolloProvider } from "@apollo/client";
import { useApollo } from "../apollo/client";
export default function App({ Component, pageProps }) {
const apolloClient = useApollo(pageProps.initialApolloState)
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
)
);
}

View file

@ -1,9 +1,9 @@
import Link from 'next/link'
import Link from "next/link";
export default function About() {
return (
<div>
Welcome to the about page. Go to the <Link href="/">Home</Link> page.
</div>
)
);
}

View file

@ -1,15 +1,15 @@
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { NextApiRequest, NextApiResponse } from 'next'
import { schema } from '../../apollo/schema'
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { NextApiRequest, NextApiResponse } from "next";
import { schema } from "../../apollo/schema";
type ExampleContext = {
req: NextApiRequest
res: NextApiResponse
}
req: NextApiRequest;
res: NextApiResponse;
};
const apolloServer = new ApolloServer<ExampleContext>({ schema })
const apolloServer = new ApolloServer<ExampleContext>({ schema });
export default startServerAndCreateNextHandler(apolloServer, {
context: async (req, res) => ({ req, res }),
})
});

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { gql, useQuery } from '@apollo/client'
import { useEffect } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { gql, useQuery } from "@apollo/client";
const ViewerQuery = gql`
query ViewerQuery {
@ -10,36 +10,36 @@ const ViewerQuery = gql`
email
}
}
`
`;
const Index = () => {
const router = useRouter()
const { data, loading, error } = useQuery(ViewerQuery)
const viewer = data?.viewer
const shouldRedirect = !(loading || error || viewer)
const router = useRouter();
const { data, loading, error } = useQuery(ViewerQuery);
const viewer = data?.viewer;
const shouldRedirect = !(loading || error || viewer);
useEffect(() => {
if (shouldRedirect) {
router.push('/signin')
router.push("/signin");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldRedirect])
}, [shouldRedirect]);
if (error) {
return <p>{error.message}</p>
return <p>{error.message}</p>;
}
if (viewer) {
return (
<div>
You're signed in as {viewer.email}. Go to{' '}
<Link href="/about">about</Link> page or{' '}
You're signed in as {viewer.email}. Go to{" "}
<Link href="/about">about</Link> page or{" "}
<Link href="/signout">signout</Link>.
</div>
)
);
}
return <p>Loading...</p>
}
return <p>Loading...</p>;
};
export default Index
export default Index;

View file

@ -1,10 +1,10 @@
import { useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { gql } from '@apollo/client'
import { useMutation, useApolloClient } from '@apollo/client'
import { getErrorMessage } from '../lib/form'
import Field from '../components/field'
import { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { gql } from "@apollo/client";
import { useMutation, useApolloClient } from "@apollo/client";
import { getErrorMessage } from "../lib/form";
import Field from "../components/field";
const SignInMutation = gql`
mutation SignInMutation($email: String!, $password: String!) {
@ -15,33 +15,33 @@ const SignInMutation = gql`
}
}
}
`
`;
function SignIn() {
const client = useApolloClient()
const [signIn] = useMutation(SignInMutation)
const [errorMsg, setErrorMsg] = useState()
const router = useRouter()
const client = useApolloClient();
const [signIn] = useMutation(SignInMutation);
const [errorMsg, setErrorMsg] = useState();
const router = useRouter();
async function handleSubmit(event) {
event.preventDefault()
event.preventDefault();
const emailElement = event.currentTarget.elements.email
const passwordElement = event.currentTarget.elements.password
const emailElement = event.currentTarget.elements.email;
const passwordElement = event.currentTarget.elements.password;
try {
await client.resetStore()
await client.resetStore();
const { data } = await signIn({
variables: {
email: emailElement.value,
password: passwordElement.value,
},
})
});
if (data.signIn.user) {
await router.push('/')
await router.push("/");
}
} catch (error) {
setErrorMsg(getErrorMessage(error))
setErrorMsg(getErrorMessage(error));
}
}
@ -64,11 +64,11 @@ function SignIn() {
required
label="Password"
/>
<button type="submit">Sign in</button> or{' '}
<button type="submit">Sign in</button> or{" "}
<Link href="/signup">Sign up</Link>
</form>
</>
)
);
}
export default SignIn
export default SignIn;

View file

@ -1,27 +1,27 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { gql, useMutation, useApolloClient } from '@apollo/client'
import { useEffect } from "react";
import { useRouter } from "next/router";
import { gql, useMutation, useApolloClient } from "@apollo/client";
const SignOutMutation = gql`
mutation SignOutMutation {
signOut
}
`
`;
function SignOut() {
const client = useApolloClient()
const router = useRouter()
const [signOut] = useMutation(SignOutMutation)
const client = useApolloClient();
const router = useRouter();
const [signOut] = useMutation(SignOutMutation);
useEffect(() => {
signOut().then(() => {
client.resetStore().then(() => {
router.push('/signin')
})
})
}, [signOut, router, client])
router.push("/signin");
});
});
}, [signOut, router, client]);
return <p>Signing out...</p>
return <p>Signing out...</p>;
}
export default SignOut
export default SignOut;

View file

@ -1,9 +1,9 @@
import { useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { gql, useMutation } from '@apollo/client'
import { getErrorMessage } from '../lib/form'
import Field from '../components/field'
import { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { gql, useMutation } from "@apollo/client";
import { getErrorMessage } from "../lib/form";
import Field from "../components/field";
const SignUpMutation = gql`
mutation SignUpMutation($email: String!, $password: String!) {
@ -14,17 +14,17 @@ const SignUpMutation = gql`
}
}
}
`
`;
function SignUp() {
const [signUp] = useMutation(SignUpMutation)
const [errorMsg, setErrorMsg] = useState()
const router = useRouter()
const [signUp] = useMutation(SignUpMutation);
const [errorMsg, setErrorMsg] = useState();
const router = useRouter();
async function handleSubmit(event) {
event.preventDefault()
const emailElement = event.currentTarget.elements.email
const passwordElement = event.currentTarget.elements.password
event.preventDefault();
const emailElement = event.currentTarget.elements.email;
const passwordElement = event.currentTarget.elements.password;
try {
await signUp({
@ -32,11 +32,11 @@ function SignUp() {
email: emailElement.value,
password: passwordElement.value,
},
})
});
router.push('/signin')
router.push("/signin");
} catch (error) {
setErrorMsg(getErrorMessage(error))
setErrorMsg(getErrorMessage(error));
}
}
@ -59,11 +59,11 @@ function SignUp() {
required
label="Password"
/>
<button type="submit">Sign up</button> or{' '}
<button type="submit">Sign up</button> or{" "}
<Link href="/signin">Sign in</Link>
</form>
</>
)
);
}
export default SignUp
export default SignUp;

View file

@ -1,54 +1,54 @@
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
import { SchemaLink } from '@apollo/client/link/schema'
import { schema } from '../apollo/schema'
import merge from 'deepmerge'
import { useMemo } from "react";
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { SchemaLink } from "@apollo/client/link/schema";
import { schema } from "../apollo/schema";
import merge from "deepmerge";
let apolloClient
let apolloClient;
function createIsomorphLink() {
if (typeof window === 'undefined') {
return new SchemaLink({ schema })
if (typeof window === "undefined") {
return new SchemaLink({ schema });
} else {
return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
uri: "/api/graphql",
credentials: "same-origin",
});
}
}
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
ssrMode: typeof window === "undefined",
link: createIsomorphLink(),
cache: new InMemoryCache(),
})
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
const _apolloClient = apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
const existingCache = _apolloClient.extract();
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = merge(initialState, existingCache)
const data = merge(initialState, existingCache);
// Restore the cache with the merged data
_apolloClient.cache.restore(data)
_apolloClient.cache.restore(data);
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
if (typeof window === "undefined") return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}

View file

@ -1,7 +1,7 @@
export const resolvers = {
Query: {
viewer() {
return { id: 1, name: 'John Smith', status: 'cached' }
return { id: 1, name: "John Smith", status: "cached" };
},
},
}
};

View file

@ -1,8 +1,8 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import { typeDefs } from './type-defs'
import { resolvers } from './resolvers'
import { makeExecutableSchema } from "@graphql-tools/schema";
import { typeDefs } from "./type-defs";
import { resolvers } from "./resolvers";
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
});

View file

@ -1,4 +1,4 @@
import { gql } from '@apollo/client'
import { gql } from "@apollo/client";
export const typeDefs = gql`
type User {
@ -10,4 +10,4 @@ export const typeDefs = gql`
type Query {
viewer: User
}
`
`;

View file

@ -1,12 +1,12 @@
import { ApolloProvider } from '@apollo/client'
import { useApollo } from '../apollo/client'
import { ApolloProvider } from "@apollo/client";
import { useApollo } from "../apollo/client";
export default function App({ Component, pageProps }) {
const apolloClient = useApollo(pageProps.initialApolloState)
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
)
);
}

View file

@ -1,9 +1,9 @@
import Link from 'next/link'
import Link from "next/link";
export default function About() {
return (
<div>
This is a static page goto <Link href="/">dynamic</Link> page.
</div>
)
);
}

View file

@ -1,7 +1,7 @@
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { schema } from '../../apollo/schema'
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { schema } from "../../apollo/schema";
const apolloServer = new ApolloServer({ schema })
const apolloServer = new ApolloServer({ schema });
export default startServerAndCreateNextHandler(apolloServer)
export default startServerAndCreateNextHandler(apolloServer);

View file

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
import Link from 'next/link'
import { useQuery } from '@apollo/client'
import { initializeApollo } from '../apollo/client'
import gql from "graphql-tag";
import Link from "next/link";
import { useQuery } from "@apollo/client";
import { initializeApollo } from "../apollo/client";
const ViewerQuery = gql`
query ViewerQuery {
@ -11,33 +11,33 @@ const ViewerQuery = gql`
status
}
}
`
`;
const Index = () => {
const {
data: { viewer },
} = useQuery(ViewerQuery)
} = useQuery(ViewerQuery);
return (
<div>
You're signed in as {viewer.name} and you're {viewer.status} goto{' '}
You're signed in as {viewer.name} and you're {viewer.status} goto{" "}
<Link href="/about">static</Link> page.
</div>
)
}
);
};
export async function getStaticProps() {
const apolloClient = initializeApollo()
const apolloClient = initializeApollo();
await apolloClient.query({
query: ViewerQuery,
})
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
},
}
};
}
export default Index
export default Index;

View file

@ -1,19 +1,19 @@
import queryGraphql from '../shared/query-graphql'
import queryGraphql from "../shared/query-graphql";
export default function UserProfile({ user }) {
if (!user) {
return <h1>User Not Found</h1>
return <h1>User Not Found</h1>;
}
return (
<h1>
{user.username} is {user.name}
</h1>
)
);
}
export async function getStaticProps(context) {
const { params } = context
const { username } = params
const { params } = context;
const { username } = params;
const { user = null } = await queryGraphql(
`
query($username: String) {
@ -23,9 +23,9 @@ export async function getStaticProps(context) {
}
}
`,
{ username }
)
return { props: { user } }
{ username },
);
return { props: { user } };
}
export async function getStaticPaths() {
@ -35,12 +35,12 @@ export async function getStaticPaths() {
username
}
}
`)) as { users: { username: string }[] }
`)) as { users: { username: string }[] };
return {
paths: users.map(({ username }) => ({
params: { username },
})),
fallback: true,
}
};
}

View file

@ -1,7 +1,7 @@
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { gql } from 'graphql-tag'
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { gql } from "graphql-tag";
const typeDefs = gql`
type Query {
@ -12,28 +12,28 @@ const typeDefs = gql`
name: String
username: String
}
`
`;
const users = [
{ name: 'Leeroy Jenkins', username: 'leeroy' },
{ name: 'Foo Bar', username: 'foobar' },
]
{ name: "Leeroy Jenkins", username: "leeroy" },
{ name: "Foo Bar", username: "foobar" },
];
const resolvers = {
Query: {
users() {
return users
return users;
},
user(parent, { username }) {
return users.find((user) => user.username === username)
return users.find((user) => user.username === username);
},
},
}
};
export const schema = makeExecutableSchema({ typeDefs, resolvers })
export const schema = makeExecutableSchema({ typeDefs, resolvers });
const server = new ApolloServer({
schema,
})
});
export default startServerAndCreateNextHandler(server)
export default startServerAndCreateNextHandler(server);

View file

@ -1,6 +1,6 @@
import Link from 'next/link'
import Link from "next/link";
import queryGraphql from '../shared/query-graphql'
import queryGraphql from "../shared/query-graphql";
export default function UserListing({ users }) {
return (
@ -16,7 +16,7 @@ export default function UserListing({ users }) {
))}
</ul>
</div>
)
);
}
export async function getStaticProps() {
@ -27,6 +27,6 @@ export async function getStaticProps() {
username
}
}
`)
return { props: { users } }
`);
return { props: { users } };
}

View file

@ -1,8 +1,8 @@
import { graphql } from 'graphql'
import { graphql } from "graphql";
import { schema } from '../../pages/api/graphql'
import { schema } from "../../pages/api/graphql";
export default async function queryGraphql(query, variableValues = {}) {
const { data } = await graphql({ schema, source: query, variableValues })
return data || {}
const { data } = await graphql({ schema, source: query, variableValues });
return data || {};
}

View file

@ -1,37 +1,37 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import Cors from 'cors'
import type { NextApiRequest, NextApiResponse } from "next";
import Cors from "cors";
// Initializing the cors middleware
// You can read more about the available options here: https://github.com/expressjs/cors#configuration-options
const cors = Cors({
methods: ['POST', 'GET', 'HEAD'],
})
methods: ["POST", "GET", "HEAD"],
});
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(
req: NextApiRequest,
res: NextApiResponse,
fn: Function
fn: Function,
) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result)
return reject(result);
}
return resolve(result)
})
})
return resolve(result);
});
});
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
res: NextApiResponse,
) {
// Run the middleware
await runMiddleware(req, res, cors)
await runMiddleware(req, res, cors);
// Rest of the API logic
res.json({ message: 'Hello Everyone!' })
res.json({ message: "Hello Everyone!" });
}

View file

@ -5,5 +5,5 @@ export default function Index() {
domain and make a POST / GET / OPTIONS request to <b>/api/cors</b>. Using
a different method from those mentioned will be blocked by CORS
</p>
)
);
}

View file

@ -1,4 +1,4 @@
import { createYoga, createSchema } from 'graphql-yoga'
import { createYoga, createSchema } from "graphql-yoga";
const typeDefs = /* GraphQL */ `
type Query {
@ -7,30 +7,30 @@ const typeDefs = /* GraphQL */ `
type User {
name: String
}
`
`;
const resolvers = {
Query: {
users() {
return [{ name: 'Nextjs' }]
return [{ name: "Nextjs" }];
},
},
}
};
const schema = createSchema({
typeDefs,
resolvers,
})
});
export const config = {
api: {
// Disable body parsing (required for file uploads)
bodyParser: false,
},
}
};
export default createYoga({
schema,
// Needed to be defined explicitly because our endpoint lives at a different path other than `/graphql`
graphqlEndpoint: '/api/graphql',
})
graphqlEndpoint: "/api/graphql",
});

View file

@ -1,30 +1,33 @@
import useSWR from 'swr'
import useSWR from "swr";
const fetcher = (query: string) =>
fetch('/api/graphql', {
method: 'POST',
fetch("/api/graphql", {
method: "POST",
headers: {
'Content-type': 'application/json',
"Content-type": "application/json",
},
body: JSON.stringify({ query }),
})
.then((res) => res.json())
.then((json) => json.data)
.then((json) => json.data);
type Data = {
users: {
name: string
}[]
}
name: string;
}[];
};
export default function Index() {
const { data, error, isLoading } = useSWR<Data>('{ users { name } }', fetcher)
const { data, error, isLoading } = useSWR<Data>(
"{ users { name } }",
fetcher,
);
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
if (!data) return null
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
const { users } = data
const { users } = data;
return (
<div>
@ -32,5 +35,5 @@ export default function Index() {
<div key={index}>{user.name}</div>
))}
</div>
)
);
}

View file

@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { setCookie } from '../../utils/cookies'
import type { NextApiRequest, NextApiResponse } from "next";
import { setCookie } from "../../utils/cookies";
export default function handler(_req: NextApiRequest, res: NextApiResponse) {
// Calling our pure function using the `res` object, it will add the `set-cookie` header
setCookie(res, 'Next.js', 'api-middleware!')
setCookie(res, "Next.js", "api-middleware!");
// Return the `set-cookie` header so we can display it in the browser and show that it works!
res.end(res.getHeader('Set-Cookie'))
res.end(res.getHeader("Set-Cookie"));
}

View file

@ -1,13 +1,13 @@
import useSWR from 'swr'
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((res) => res.text())
const fetcher = (url: string) => fetch(url).then((res) => res.text());
export default function Index() {
const { data, error, isLoading } = useSWR<string>('/api/cookies', fetcher)
const { data, error, isLoading } = useSWR<string>("/api/cookies", fetcher);
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
if (!data) return null
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
return <div>{`Cookie from response: "${data}"`}</div>
return <div>{`Cookie from response: "${data}"`}</div>;
}

View file

@ -1,5 +1,5 @@
import type { NextApiResponse } from 'next'
import { serialize, CookieSerializeOptions } from 'cookie'
import type { NextApiResponse } from "next";
import { serialize, CookieSerializeOptions } from "cookie";
/**
* This sets `cookie` using the `res` object
@ -9,14 +9,14 @@ export const setCookie = (
res: NextApiResponse,
name: string,
value: unknown,
options: CookieSerializeOptions = {}
options: CookieSerializeOptions = {},
) => {
const stringValue =
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
typeof value === "object" ? "j:" + JSON.stringify(value) : String(value);
if (typeof options.maxAge === 'number') {
options.expires = new Date(Date.now() + options.maxAge * 1000)
if (typeof options.maxAge === "number") {
options.expires = new Date(Date.now() + options.maxAge * 1000);
}
res.setHeader('Set-Cookie', serialize(name, stringValue, options))
}
res.setHeader("Set-Cookie", serialize(name, stringValue, options));
};

View file

@ -1,20 +1,20 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { v4 as uuidv4 } from 'uuid'
import rateLimit from '../../utils/rate-limit'
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 as uuidv4 } from "uuid";
import rateLimit from "../../utils/rate-limit";
const limiter = rateLimit({
interval: 60 * 1000, // 60 seconds
uniqueTokenPerInterval: 500, // Max 500 users per second
})
});
export default async function handler(
_req: NextApiRequest,
res: NextApiResponse
res: NextApiResponse,
) {
try {
await limiter.check(res, 10, 'CACHE_TOKEN') // 10 requests per minute
res.status(200).json({ id: uuidv4() })
await limiter.check(res, 10, "CACHE_TOKEN"); // 10 requests per minute
res.status(200).json({ id: uuidv4() });
} catch {
res.status(429).json({ error: 'Rate limit exceeded' })
res.status(429).json({ error: "Rate limit exceeded" });
}
}

View file

@ -1,25 +1,27 @@
import { useState } from 'react'
import styles from '../styles.module.css'
import { useState } from "react";
import styles from "../styles.module.css";
export default function Index() {
const [response, setResponse] = useState<Record<string, unknown> | null>(null)
const [response, setResponse] = useState<Record<string, unknown> | null>(
null,
);
const makeRequest = async () => {
const res = await fetch('/api/user')
const res = await fetch("/api/user");
setResponse({
status: res.status,
body: await res.json(),
limit: res.headers.get('X-RateLimit-Limit'),
remaining: res.headers.get('X-RateLimit-Remaining'),
})
}
limit: res.headers.get("X-RateLimit-Limit"),
remaining: res.headers.get("X-RateLimit-Remaining"),
});
};
return (
<main className={styles.container}>
<h1>Next.js API Routes Rate Limiting</h1>
<p>
This example uses <code className={styles.inlineCode}>lru-cache</code>{' '}
This example uses <code className={styles.inlineCode}>lru-cache</code>{" "}
to implement a simple rate limiter for API routes (Serverless
Functions).
</p>
@ -30,5 +32,5 @@ export default function Index() {
</code>
)}
</main>
)
);
}

View file

@ -47,7 +47,7 @@
.inlineCode::before,
.inlineCode::after {
content: '`';
content: "`";
}
.code {

View file

@ -1,35 +1,35 @@
import type { NextApiResponse } from 'next'
import { LRUCache } from 'lru-cache'
import type { NextApiResponse } from "next";
import { LRUCache } from "lru-cache";
type Options = {
uniqueTokenPerInterval?: number
interval?: number
}
uniqueTokenPerInterval?: number;
interval?: number;
};
export default function rateLimit(options?: Options) {
const tokenCache = new LRUCache({
max: options?.uniqueTokenPerInterval || 500,
ttl: options?.interval || 60000,
})
});
return {
check: (res: NextApiResponse, limit: number, token: string) =>
new Promise<void>((resolve, reject) => {
const tokenCount = (tokenCache.get(token) as number[]) || [0]
const tokenCount = (tokenCache.get(token) as number[]) || [0];
if (tokenCount[0] === 0) {
tokenCache.set(token, tokenCount)
tokenCache.set(token, tokenCount);
}
tokenCount[0] += 1
tokenCount[0] += 1;
const currentUsage = tokenCount[0]
const isRateLimited = currentUsage >= limit
res.setHeader('X-RateLimit-Limit', limit)
const currentUsage = tokenCount[0];
const isRateLimited = currentUsage >= limit;
res.setHeader("X-RateLimit-Limit", limit);
res.setHeader(
'X-RateLimit-Remaining',
isRateLimited ? 0 : limit - currentUsage
)
"X-RateLimit-Remaining",
isRateLimited ? 0 : limit - currentUsage,
);
return isRateLimited ? reject() : resolve()
return isRateLimited ? reject() : resolve();
}),
}
};
}

View file

@ -1,4 +1,4 @@
export type User = {
id: number
name?: string
}
id: number;
name?: string;
};

View file

@ -1,25 +1,25 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import type { User } from '../../../interfaces'
import type { NextApiRequest, NextApiResponse } from "next";
import type { User } from "../../../interfaces";
export default function userHandler(
req: NextApiRequest,
res: NextApiResponse<User>
res: NextApiResponse<User>,
) {
const { query, method } = req
const id = parseInt(query.id as string, 10)
const name = query.name as string
const { query, method } = req;
const id = parseInt(query.id as string, 10);
const name = query.name as string;
switch (method) {
case 'GET':
case "GET":
// Get data from your database
res.status(200).json({ id, name: `User ${id}` })
break
case 'PUT':
res.status(200).json({ id, name: `User ${id}` });
break;
case "PUT":
// Update or create data in your database
res.status(200).json({ id, name: name || `User ${id}` })
break
res.status(200).json({ id, name: name || `User ${id}` });
break;
default:
res.setHeader('Allow', ['GET', 'PUT'])
res.status(405).end(`Method ${method} Not Allowed`)
res.setHeader("Allow", ["GET", "PUT"]);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

View file

@ -1,13 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import type { User } from '../../interfaces'
import type { NextApiRequest, NextApiResponse } from "next";
import type { User } from "../../interfaces";
// Fake users data
const users: User[] = [{ id: 1 }, { id: 2 }, { id: 3 }]
const users: User[] = [{ id: 1 }, { id: 2 }, { id: 3 }];
export default function handler(
_req: NextApiRequest,
res: NextApiResponse<User[]>
res: NextApiResponse<User[]>,
) {
// Get data from your database
res.status(200).json(users)
res.status(200).json(users);
}

View file

@ -1,15 +1,15 @@
import type { User } from '../interfaces'
import useSwr from 'swr'
import Link from 'next/link'
import type { User } from "../interfaces";
import useSwr from "swr";
import Link from "next/link";
const fetcher = (url: string) => fetch(url).then((res) => res.json())
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export default function Index() {
const { data, error, isLoading } = useSwr<User[]>('/api/users', fetcher)
const { data, error, isLoading } = useSwr<User[]>("/api/users", fetcher);
if (error) return <div>Failed to load users</div>
if (isLoading) return <div>Loading...</div>
if (!data) return null
if (error) return <div>Failed to load users</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
return (
<ul>
@ -21,5 +21,5 @@ export default function Index() {
</li>
))}
</ul>
)
);
}

View file

@ -1,19 +1,19 @@
import type { User } from '../../interfaces'
import { useRouter } from 'next/router'
import useSwr from 'swr'
import type { User } from "../../interfaces";
import { useRouter } from "next/router";
import useSwr from "swr";
const fetcher = (url: string) => fetch(url).then((res) => res.json())
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export default function UserPage() {
const { query } = useRouter()
const { query } = useRouter();
const { data, error, isLoading } = useSwr<User>(
query.id ? `/api/user/${query.id}` : null,
fetcher
)
fetcher,
);
if (error) return <div>Failed to load user</div>
if (isLoading) return <div>Loading...</div>
if (!data) return null
if (error) return <div>Failed to load user</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
return <div>{data.name}</div>
return <div>{data.name}</div>;
}

View file

@ -1,9 +1,9 @@
import Link from 'next/link'
import { Person } from '../interfaces'
import Link from "next/link";
import { Person } from "../interfaces";
type PersonProps = {
person: Person
}
person: Person;
};
export default function PersonComponent({ person }: PersonProps) {
return (
@ -12,5 +12,5 @@ export default function PersonComponent({ person }: PersonProps) {
{person.name}
</Link>
</li>
)
);
}

View file

@ -1,102 +1,102 @@
export const people = [
{
id: '1',
name: 'Luke Skywalker',
height: '172',
mass: '77',
hair_color: 'blond',
skin_color: 'fair',
eye_color: 'blue',
gender: 'male',
id: "1",
name: "Luke Skywalker",
height: "172",
mass: "77",
hair_color: "blond",
skin_color: "fair",
eye_color: "blue",
gender: "male",
},
{
id: '2',
name: 'C-3PO',
height: '167',
mass: '75',
hair_color: 'n/a',
skin_color: 'gold',
eye_color: 'yellow',
gender: 'n/a',
id: "2",
name: "C-3PO",
height: "167",
mass: "75",
hair_color: "n/a",
skin_color: "gold",
eye_color: "yellow",
gender: "n/a",
},
{
id: '3',
name: 'R2-D2',
height: '96',
mass: '32',
hair_color: 'n/a',
skin_color: 'white, blue',
eye_color: 'red',
gender: 'n/a',
id: "3",
name: "R2-D2",
height: "96",
mass: "32",
hair_color: "n/a",
skin_color: "white, blue",
eye_color: "red",
gender: "n/a",
},
{
id: '4',
name: 'Darth Vader',
height: '202',
mass: '136',
hair_color: 'none',
skin_color: 'white',
eye_color: 'yellow',
gender: 'male',
id: "4",
name: "Darth Vader",
height: "202",
mass: "136",
hair_color: "none",
skin_color: "white",
eye_color: "yellow",
gender: "male",
},
{
id: '5',
name: 'Leia Organa',
height: '150',
mass: '49',
hair_color: 'brown',
skin_color: 'light',
eye_color: 'brown',
gender: 'female',
id: "5",
name: "Leia Organa",
height: "150",
mass: "49",
hair_color: "brown",
skin_color: "light",
eye_color: "brown",
gender: "female",
},
{
id: '6',
name: 'Owen Lars',
height: '178',
mass: '120',
hair_color: 'brown, grey',
skin_color: 'light',
eye_color: 'blue',
gender: 'male',
id: "6",
name: "Owen Lars",
height: "178",
mass: "120",
hair_color: "brown, grey",
skin_color: "light",
eye_color: "blue",
gender: "male",
},
{
id: '7',
name: 'Beru Whitesun Lars',
height: '165',
mass: '75',
hair_color: 'brown',
skin_color: 'light',
eye_color: 'blue',
gender: 'female',
id: "7",
name: "Beru Whitesun Lars",
height: "165",
mass: "75",
hair_color: "brown",
skin_color: "light",
eye_color: "blue",
gender: "female",
},
{
id: '8',
name: 'R5-D4',
height: '97',
mass: '32',
hair_color: 'n/a',
skin_color: 'white, red',
eye_color: 'red',
gender: 'n/a',
id: "8",
name: "R5-D4",
height: "97",
mass: "32",
hair_color: "n/a",
skin_color: "white, red",
eye_color: "red",
gender: "n/a",
},
{
id: '9',
name: 'Biggs Darklighter',
height: '183',
mass: '84',
hair_color: 'black',
skin_color: 'light',
eye_color: 'brown',
gender: 'male',
id: "9",
name: "Biggs Darklighter",
height: "183",
mass: "84",
hair_color: "black",
skin_color: "light",
eye_color: "brown",
gender: "male",
},
{
id: '10',
name: 'Obi-Wan Kenobi',
height: '182',
mass: '77',
hair_color: 'auburn, white',
skin_color: 'fair',
eye_color: 'blue-gray',
gender: 'male',
id: "10",
name: "Obi-Wan Kenobi",
height: "182",
mass: "77",
hair_color: "auburn, white",
skin_color: "fair",
eye_color: "blue-gray",
gender: "male",
},
]
];

View file

@ -1,14 +1,14 @@
export type Person = {
id: string
name: string
height: string
mass: string
hair_color: string
skin_color: string
eye_color: string
gender: string
}
id: string;
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
eye_color: string;
gender: string;
};
export type ResponseError = {
message: string
}
message: string;
};

View file

@ -1,17 +1,17 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { people } from '../../../data'
import type { Person, ResponseError } from '../../../interfaces'
import { NextApiRequest, NextApiResponse } from "next";
import { people } from "../../../data";
import type { Person, ResponseError } from "../../../interfaces";
export default function personHandler(
req: NextApiRequest,
res: NextApiResponse<Person | ResponseError>
res: NextApiResponse<Person | ResponseError>,
) {
const { query } = req
const { id } = query
const person = people.find((p) => p.id === id)
const { query } = req;
const { id } = query;
const person = people.find((p) => p.id === id);
// User with id exists
return person
? res.status(200).json(person)
: res.status(404).json({ message: `User with id: ${id} not found.` })
: res.status(404).json({ message: `User with id: ${id} not found.` });
}

View file

@ -1,10 +1,10 @@
import { NextApiResponse, NextApiRequest } from 'next'
import { people } from '../../../data'
import { Person } from '../../../interfaces'
import { NextApiResponse, NextApiRequest } from "next";
import { people } from "../../../data";
import { Person } from "../../../interfaces";
export default function handler(
_req: NextApiRequest,
res: NextApiResponse<Person[]>
res: NextApiResponse<Person[]>,
) {
return res.status(200).json(people)
return res.status(200).json(people);
}

View file

@ -1,15 +1,15 @@
import useSWR from 'swr'
import PersonComponent from '../components/Person'
import type { Person } from '../interfaces'
import useSWR from "swr";
import PersonComponent from "../components/Person";
import type { Person } from "../interfaces";
const fetcher = (url: string) => fetch(url).then((res) => res.json())
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export default function Index() {
const { data, error, isLoading } = useSWR<Person[]>('/api/people', fetcher)
const { data, error, isLoading } = useSWR<Person[]>("/api/people", fetcher);
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
if (!data) return null
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
return (
<ul>
@ -17,5 +17,5 @@ export default function Index() {
<PersonComponent key={p.id} person={p} />
))}
</ul>
)
);
}

View file

@ -1,27 +1,27 @@
import { useRouter } from 'next/router'
import useSWR from 'swr'
import type { Person, ResponseError } from '../../interfaces'
import { useRouter } from "next/router";
import useSWR from "swr";
import type { Person, ResponseError } from "../../interfaces";
const fetcher = async (url: string) => {
const res = await fetch(url)
const data = await res.json()
const res = await fetch(url);
const data = await res.json();
if (res.status !== 200) {
throw new Error(data.message)
throw new Error(data.message);
}
return data
}
return data;
};
export default function PersonPage() {
const { query } = useRouter()
const { query } = useRouter();
const { data, error, isLoading, isValidating } = useSWR<
Person,
ResponseError
>(() => (query.id ? `/api/people/${query.id}` : null), fetcher)
>(() => (query.id ? `/api/people/${query.id}` : null), fetcher);
if (error) return <div>{error.message}</div>
if (isLoading) return <div>Loading...</div>
if (!data) return null
if (error) return <div>{error.message}</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
return (
<table>
@ -56,5 +56,5 @@ export default function PersonPage() {
</tr>
</tbody>
</table>
)
);
}

View file

@ -1,14 +1,14 @@
'use client'
"use client";
import { useState } from 'react'
import { type getDictionary } from '../../../get-dictionary'
import { useState } from "react";
import { type getDictionary } from "../../../get-dictionary";
export default function Counter({
dictionary,
}: {
dictionary: Awaited<ReturnType<typeof getDictionary>>['counter']
dictionary: Awaited<ReturnType<typeof getDictionary>>["counter"];
}) {
const [count, setCount] = useState(0)
const [count, setCount] = useState(0);
return (
<p>
This component is rendered on client:
@ -20,5 +20,5 @@ export default function Counter({
{dictionary.increment}
</button>
</p>
)
);
}

View file

@ -1,17 +1,17 @@
'use client'
"use client";
import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { i18n, type Locale } from '../../../i18n-config'
import { usePathname } from "next/navigation";
import Link from "next/link";
import { i18n, type Locale } from "../../../i18n-config";
export default function LocaleSwitcher() {
const pathName = usePathname()
const pathName = usePathname();
const redirectedPathName = (locale: Locale) => {
if (!pathName) return '/'
const segments = pathName.split('/')
segments[1] = locale
return segments.join('/')
}
if (!pathName) return "/";
const segments = pathName.split("/");
segments[1] = locale;
return segments.join("/");
};
return (
<div>
@ -22,9 +22,9 @@ export default function LocaleSwitcher() {
<li key={locale}>
<Link href={redirectedPathName(locale)}>{locale}</Link>
</li>
)
);
})}
</ul>
</div>
)
);
}

View file

@ -1,24 +1,24 @@
import { i18n, type Locale } from '../../i18n-config'
import { i18n, type Locale } from "../../i18n-config";
export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale }))
return i18n.locales.map((locale) => ({ lang: locale }));
}
export default function Root({
children,
params,
}: {
children: React.ReactNode
params: { lang: Locale }
children: React.ReactNode;
params: { lang: Locale };
}) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
);
}
export const metadata = {
title: 'i18n within app directory - Vercel Examples',
description: 'How to do i18n in Next.js 13 within app directory',
}
title: "i18n within app directory - Vercel Examples",
description: "How to do i18n in Next.js 13 within app directory",
};

View file

@ -1,24 +1,24 @@
import { getDictionary } from '../../get-dictionary'
import { Locale } from '../../i18n-config'
import Counter from './components/counter'
import LocaleSwitcher from './components/locale-switcher'
import { getDictionary } from "../../get-dictionary";
import { Locale } from "../../i18n-config";
import Counter from "./components/counter";
import LocaleSwitcher from "./components/locale-switcher";
export default async function IndexPage({
params: { lang },
}: {
params: { lang: Locale }
params: { lang: Locale };
}) {
const dictionary = await getDictionary(lang)
const dictionary = await getDictionary(lang);
return (
<div>
<LocaleSwitcher />
<p>Current locale: {lang}</p>
<p>
This text is rendered on the server:{' '}
{dictionary['server-component'].welcome}
This text is rendered on the server:{" "}
{dictionary["server-component"].welcome}
</p>
<Counter dictionary={dictionary.counter} />
</div>
)
);
}

View file

@ -1,13 +1,13 @@
import 'server-only'
import type { Locale } from './i18n-config'
import "server-only";
import type { Locale } from "./i18n-config";
// We enumerate all dictionaries here for better linting and typescript support
// We also get the default import for cleaner types
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
de: () => import('./dictionaries/de.json').then((module) => module.default),
cs: () => import('./dictionaries/cs.json').then((module) => module.default),
}
en: () => import("./dictionaries/en.json").then((module) => module.default),
de: () => import("./dictionaries/de.json").then((module) => module.default),
cs: () => import("./dictionaries/cs.json").then((module) => module.default),
};
export const getDictionary = async (locale: Locale) =>
dictionaries[locale]?.() ?? dictionaries.en()
dictionaries[locale]?.() ?? dictionaries.en();

View file

@ -1,6 +1,6 @@
export const i18n = {
defaultLocale: 'en',
locales: ['en', 'de', 'cs'],
} as const
defaultLocale: "en",
locales: ["en", "de", "cs"],
} as const;
export type Locale = (typeof i18n)['locales'][number]
export type Locale = (typeof i18n)["locales"][number];

View file

@ -1,31 +1,31 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { i18n } from './i18n-config'
import { i18n } from "./i18n-config";
import { match as matchLocale } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales
const locales: string[] = i18n.locales;
// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
locales
)
locales,
);
const locale = matchLocale(languages, locales, i18n.defaultLocale)
const locale = matchLocale(languages, locales, i18n.defaultLocale);
return locale
return locale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
const pathname = request.nextUrl.pathname;
// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// // If you have one
@ -40,25 +40,26 @@ export function middleware(request: NextRequest) {
// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
)
(locale) =>
!pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request)
const locale = getLocale(request);
// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
)
`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
request.url,
),
);
}
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

View file

@ -1,9 +1,9 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;

View file

@ -1,18 +1,18 @@
import './globals.css'
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
);
}
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
title: "Create Next App",
description: "Generated by create next app",
};

View file

@ -102,7 +102,7 @@
.center::before,
.center::after {
content: '';
content: "";
left: 50%;
position: absolute;
filter: blur(45px);
@ -130,7 +130,7 @@
.thirteen::before,
.thirteen::after {
content: '';
content: "";
position: absolute;
z-index: -1;
}

View file

@ -1,9 +1,9 @@
import Image from 'next/image'
import { Inter } from 'next/font/google'
import styles from './page.module.css'
import Content from './message.mdx'
import Image from "next/image";
import { Inter } from "next/font/google";
import styles from "./page.module.css";
import Content from "./message.mdx";
const inter = Inter({ subsets: ['latin'] })
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
return (
@ -19,7 +19,7 @@ export default function Home() {
target="_blank"
rel="noopener noreferrer"
>
By{' '}
By{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
@ -78,5 +78,5 @@ export default function Home() {
</a>
</div>
</main>
)
);
}

View file

@ -1,4 +1,4 @@
import type { MDXComponents } from 'mdx/types'
import type { MDXComponents } from "mdx/types";
// This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents {
@ -6,5 +6,5 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
// Allows customizing built-in components, e.g. to add styling.
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
...components,
}
};
}

View file

@ -1,10 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"],
experimental: {
mdxRs: true,
},
}
};
const withMDX = require('@next/mdx')()
module.exports = withMDX(nextConfig)
const withMDX = require("@next/mdx")();
module.exports = withMDX(nextConfig);

View file

@ -1,5 +1,5 @@
// types/mdx.d.ts
declare module '*.mdx' {
let MDXComponent: (props) => JSX.Element
export default MDXComponent
declare module "*.mdx" {
let MDXComponent: (props) => JSX.Element;
export default MDXComponent;
}

View file

@ -1,14 +1,14 @@
import React from 'react'
import styles from '../styles/Home.module.css'
import { LoginMethod } from '../lib/types'
import StytchContainer from './StytchContainer'
import React from "react";
import styles from "../styles/Home.module.css";
import { LoginMethod } from "../lib/types";
import StytchContainer from "./StytchContainer";
type Props = {
setLoginMethod: (loginMethod: LoginMethod) => void
}
setLoginMethod: (loginMethod: LoginMethod) => void;
};
const LoginEntryPoint = (props: Props) => {
const { setLoginMethod } = props
const { setLoginMethod } = props;
return (
<StytchContainer>
<h2>Hello Vercel!</h2>
@ -29,7 +29,7 @@ const LoginEntryPoint = (props: Props) => {
API Integration (SMS Passcodes)
</button>
</StytchContainer>
)
}
);
};
export default LoginEntryPoint
export default LoginEntryPoint;

View file

@ -1,12 +1,12 @@
import React, { useState } from 'react'
import SendOTPForm from './SendOTPForm'
import VerifyOTPForm from './VerifyOTPForm'
import StytchContainer from './StytchContainer'
import React, { useState } from "react";
import SendOTPForm from "./SendOTPForm";
import VerifyOTPForm from "./VerifyOTPForm";
import StytchContainer from "./StytchContainer";
const LoginWithSMS = () => {
const [otpSent, setOTPSent] = useState(false)
const [phoneNumber, setPhoneNumber] = useState('')
const [methodId, setMethodId] = useState('')
const [otpSent, setOTPSent] = useState(false);
const [phoneNumber, setPhoneNumber] = useState("");
const [methodId, setMethodId] = useState("");
return (
<StytchContainer>
@ -21,7 +21,7 @@ const LoginWithSMS = () => {
<VerifyOTPForm methodId={methodId} phoneNumber={phoneNumber} />
)}
</StytchContainer>
)
}
);
};
export default LoginWithSMS
export default LoginWithSMS;

View file

@ -1,44 +1,44 @@
import React from 'react'
import { sendOTP } from '../lib/otpUtils'
import styles from '../styles/Home.module.css'
import React from "react";
import { sendOTP } from "../lib/otpUtils";
import styles from "../styles/Home.module.css";
type Props = {
phoneNumber: string
setMethodId: (methodId: string) => void
setOTPSent: (submitted: boolean) => void
setPhoneNumber: (phoneNumber: string) => void
}
phoneNumber: string;
setMethodId: (methodId: string) => void;
setOTPSent: (submitted: boolean) => void;
setPhoneNumber: (phoneNumber: string) => void;
};
const SendOTPForm = (props: Props): JSX.Element => {
const { phoneNumber, setMethodId, setOTPSent, setPhoneNumber } = props
const [isDisabled, setIsDisabled] = React.useState(true)
const { phoneNumber, setMethodId, setOTPSent, setPhoneNumber } = props;
const [isDisabled, setIsDisabled] = React.useState(true);
const isValidNumber = (phoneNumberValue: string) => {
// Regex validates phone numbers in (xxx)xxx-xxxx, xxx-xxx-xxxx, xxxxxxxxxx, and xxx.xxx.xxxx format
const regex = /^[(]?[0-9]{3}[)]?[-s.]?[0-9]{3}[-s.]?[0-9]{4}$/g
const regex = /^[(]?[0-9]{3}[)]?[-s.]?[0-9]{3}[-s.]?[0-9]{4}$/g;
if (phoneNumberValue.match(regex)) {
return true
return true;
}
return false
}
return false;
};
const onPhoneNumberChange = (e: React.ChangeEvent<{ value: string }>) => {
setPhoneNumber(e.target.value)
setPhoneNumber(e.target.value);
if (isValidNumber(e.target.value)) {
setIsDisabled(false)
setIsDisabled(false);
} else {
setIsDisabled(true)
setIsDisabled(true);
}
}
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
e.preventDefault();
if (isValidNumber(phoneNumber)) {
const methodId = await sendOTP(phoneNumber)
setMethodId(methodId)
setOTPSent(true)
const methodId = await sendOTP(phoneNumber);
setMethodId(methodId);
setOTPSent(true);
}
}
};
return (
<div>
@ -77,7 +77,7 @@ const SendOTPForm = (props: Props): JSX.Element => {
/>
</form>
</div>
)
}
);
};
export default SendOTPForm
export default SendOTPForm;

View file

@ -1,20 +1,20 @@
import React from 'react'
import styles from '../styles/Home.module.css'
import Image from 'next/image'
import lockup from '/public/powered-by-stytch.svg'
import React from "react";
import styles from "../styles/Home.module.css";
import Image from "next/image";
import lockup from "/public/powered-by-stytch.svg";
type Props = {
children: React.ReactElement | React.ReactElement[]
}
children: React.ReactElement | React.ReactElement[];
};
const StytchContainer = (props: Props) => {
const { children } = props
const { children } = props;
return (
<div className={styles.container}>
<div>{children}</div>
<Image alt="Powered by Stytch" height={15} src={lockup} width={150} />
</div>
)
}
);
};
export default StytchContainer
export default StytchContainer;

View file

@ -1,113 +1,113 @@
import React from 'react'
import styles from '../styles/Home.module.css'
import { sendOTP } from '../lib/otpUtils'
import { useRouter } from 'next/router'
import React from "react";
import styles from "../styles/Home.module.css";
import { sendOTP } from "../lib/otpUtils";
import { useRouter } from "next/router";
// Handles auto-tabbing to next passcode digit input.
// Logic inspired from https://stackoverflow.com/questions/15595652/focus-next-input-once-reaching-maxlength-value.
const autoTab = (target: HTMLInputElement, key?: string) => {
if (target.value.length >= target.maxLength) {
let next = target
let next = target;
while ((next = next.nextElementSibling as HTMLInputElement)) {
if (next == null) break
if (next.tagName.toLowerCase() === 'input') {
next?.focus()
break
if (next == null) break;
if (next.tagName.toLowerCase() === "input") {
next?.focus();
break;
}
}
}
// Move to previous field if empty (user pressed backspace)
else if (target.value.length === 0) {
let previous = target
let previous = target;
while ((previous = previous.previousElementSibling as HTMLInputElement)) {
if (previous == null) break
if (previous.tagName.toLowerCase() === 'input') {
previous.focus()
break
if (previous == null) break;
if (previous.tagName.toLowerCase() === "input") {
previous.focus();
break;
}
}
}
}
};
type Props = {
methodId: string
phoneNumber: string
}
methodId: string;
phoneNumber: string;
};
const VerifyOTPForm = (props: Props) => {
const { methodId, phoneNumber } = props
const [isDisabled, setIsDisabled] = React.useState(true)
const [currentMethodId, setCurrentMethodId] = React.useState(methodId)
const [isError, setIsError] = React.useState(false)
const router = useRouter()
const { methodId, phoneNumber } = props;
const [isDisabled, setIsDisabled] = React.useState(true);
const [currentMethodId, setCurrentMethodId] = React.useState(methodId);
const [isError, setIsError] = React.useState(false);
const router = useRouter();
const strippedNumber = phoneNumber.replace(/\D/g, '')
const strippedNumber = phoneNumber.replace(/\D/g, "");
const parsedPhoneNumber = `(${strippedNumber.slice(
0,
3
)}) ${strippedNumber.slice(3, 6)}-${strippedNumber.slice(6, 10)}`
3,
)}) ${strippedNumber.slice(3, 6)}-${strippedNumber.slice(6, 10)}`;
const isValidPasscode = () => {
const regex = /^[0-9]$/g
const inputs = document.getElementsByClassName(styles.passcodeInput)
const regex = /^[0-9]$/g;
const inputs = document.getElementsByClassName(styles.passcodeInput);
for (let i = 0; i < inputs.length; i++) {
if (!(inputs[i] as HTMLInputElement).value.match(regex)) {
return false
return false;
}
}
return true
}
return true;
};
const onPasscodeDigitChange = () => {
if (isValidPasscode()) {
setIsDisabled(false)
setIsError(false)
setIsDisabled(false);
setIsError(false);
} else {
setIsDisabled(true)
setIsDisabled(true);
}
}
};
const resetPasscode = () => {
const inputs = document.getElementsByClassName(styles.passcodeInput)
const inputs = document.getElementsByClassName(styles.passcodeInput);
for (let i = 0; i < inputs.length; i++) {
;(inputs[i] as HTMLInputElement).value = ''
(inputs[i] as HTMLInputElement).value = "";
}
document.getElementById('digit-0')?.focus()
setIsDisabled(true)
}
document.getElementById("digit-0")?.focus();
setIsDisabled(true);
};
const resendCode = async () => {
const methodId = await sendOTP(phoneNumber)
setCurrentMethodId(methodId)
resetPasscode()
setIsError(false)
}
const methodId = await sendOTP(phoneNumber);
setCurrentMethodId(methodId);
resetPasscode();
setIsError(false);
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
e.preventDefault();
if (isValidPasscode()) {
let otpInput = ''
const inputs = document.getElementsByClassName(styles.passcodeInput)
let otpInput = "";
const inputs = document.getElementsByClassName(styles.passcodeInput);
for (let i = 0; i < inputs.length; i++) {
otpInput += (inputs[i] as HTMLInputElement).value
otpInput += (inputs[i] as HTMLInputElement).value;
}
const resp = await fetch('/api/authenticate_otp', {
method: 'POST',
const resp = await fetch("/api/authenticate_otp", {
method: "POST",
body: JSON.stringify({ otpInput, methodId: currentMethodId }),
})
});
if (resp.status === 200) {
router.push('/profile')
router.push("/profile");
} else {
setIsError(true)
resetPasscode()
setIsError(true);
resetPasscode();
}
}
}
};
const renderPasscodeInputs = () => {
const inputs = []
const inputs = [];
for (let i = 0; i < 6; i += 1) {
inputs.push(
<input
@ -121,23 +121,23 @@ const VerifyOTPForm = (props: Props) => {
placeholder="0"
size={1}
type="text"
/>
)
/>,
);
}
return inputs
}
return inputs;
};
return (
<div>
<h2>Enter passcode</h2>
<p className={styles.smsInstructions}>
A 6-digit passcode was sent to you at{' '}
A 6-digit passcode was sent to you at{" "}
<strong>{parsedPhoneNumber}</strong>.
</p>
<form onSubmit={onSubmit}>
<div className={styles.passcodeContainer}>
<p className={styles.errorText}>
{isError ? 'Invalid code. Please try again.' : ''}
{isError ? "Invalid code. Please try again." : ""}
</p>
<div className={styles.passcodeInputContainer}>
{renderPasscodeInputs()}
@ -162,7 +162,7 @@ const VerifyOTPForm = (props: Props) => {
/>
</form>
</div>
)
}
);
};
export default VerifyOTPForm
export default VerifyOTPForm;

View file

@ -1,19 +1,19 @@
import * as stytch from 'stytch'
import * as stytch from "stytch";
let client: stytch.Client
let client: stytch.Client;
const loadStytch = () => {
if (!client) {
client = new stytch.Client({
project_id: process.env.STYTCH_PROJECT_ID || '',
secret: process.env.STYTCH_SECRET || '',
project_id: process.env.STYTCH_PROJECT_ID || "",
secret: process.env.STYTCH_SECRET || "",
env:
process.env.STYTCH_PROJECT_ENV === 'live'
process.env.STYTCH_PROJECT_ENV === "live"
? stytch.envs.live
: stytch.envs.test,
})
});
}
return client
}
return client;
};
export default loadStytch
export default loadStytch;

View file

@ -1,11 +1,11 @@
export async function sendOTP(phoneNumber: string) {
const resp = await fetch('/api/send_otp', {
method: 'POST',
const resp = await fetch("/api/send_otp", {
method: "POST",
body: JSON.stringify({
intlCode: '+1',
intlCode: "+1",
phoneNumber,
}),
})
const data = await resp.json()
return data.methodId
});
const data = await resp.json();
return data.methodId;
}

View file

@ -1,28 +1,28 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { Session, withIronSession } from 'next-iron-session'
import { NextApiRequest, NextApiResponse } from "next";
import { Session, withIronSession } from "next-iron-session";
type NextIronRequest = NextApiRequest & { session: Session }
type NextIronRequest = NextApiRequest & { session: Session };
type APIHandler = (
req: NextIronRequest,
res: NextApiResponse<any>
) => Promise<any>
res: NextApiResponse<any>,
) => Promise<any>;
export type ServerSideProps = ({
req,
}: {
req: NextIronRequest
}) => Promise<any>
req: NextIronRequest;
}) => Promise<any>;
const withSession = (handler: APIHandler | ServerSideProps) =>
withIronSession(handler, {
password: process.env.IRON_SESSION_PASSWORD || '',
cookieName: process.env.IRON_SESSION_COOKIE_NAME || '',
password: process.env.IRON_SESSION_PASSWORD || "",
cookieName: process.env.IRON_SESSION_COOKIE_NAME || "",
// if your localhost is served on http:// then disable the secure flag
cookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
secure: process.env.NODE_ENV === "production",
},
})
});
export default withSession
export default withSession;

View file

@ -1,11 +1,11 @@
import '../styles/globals.css'
import styles from '../styles/Home.module.css'
import type { AppProps } from 'next/app'
import React from 'react'
import Head from 'next/head'
import Image from 'next/image'
import stytchLogo from '/public/stytch-logo.svg'
import vercelLogo from '/public/vercel-logotype-dark.svg'
import "../styles/globals.css";
import styles from "../styles/Home.module.css";
import type { AppProps } from "next/app";
import React from "react";
import Head from "next/head";
import Image from "next/image";
import stytchLogo from "/public/stytch-logo.svg";
import vercelLogo from "/public/vercel-logotype-dark.svg";
function MyApp({ Component, pageProps }: AppProps) {
return (
@ -50,6 +50,6 @@ function MyApp({ Component, pageProps }: AppProps) {
<Component {...pageProps} />
</div>
</React.Fragment>
)
);
}
export default MyApp
export default MyApp;

View file

@ -1,4 +1,4 @@
import { Html, Head, Main, NextScript } from 'next/document'
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
@ -14,5 +14,5 @@ export default function Document() {
<NextScript />
</body>
</Html>
)
);
}

View file

@ -1,39 +1,39 @@
// This API route authenticates a Stytch magic link.
import type { NextApiRequest, NextApiResponse } from 'next'
import { Session } from 'next-iron-session'
import withSession from '../../lib/withSession'
import loadStytch from '../../lib/loadStytch'
type NextIronRequest = NextApiRequest & { session: Session }
import type { NextApiRequest, NextApiResponse } from "next";
import { Session } from "next-iron-session";
import withSession from "../../lib/withSession";
import loadStytch from "../../lib/loadStytch";
type NextIronRequest = NextApiRequest & { session: Session };
type Data = {
errorString: string
}
errorString: string;
};
export async function handler(
req: NextIronRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
if (req.method === 'GET') {
const client = loadStytch()
const { token } = req.query
if (req.method === "GET") {
const client = loadStytch();
const { token } = req.query;
try {
const resp = await client.magicLinks.authenticate(token as string)
const resp = await client.magicLinks.authenticate(token as string);
// Set session
req.session.destroy()
req.session.set('user', {
req.session.destroy();
req.session.set("user", {
id: resp.user_id,
})
});
// Save additional user data here
await req.session.save()
res.redirect('/profile')
await req.session.save();
res.redirect("/profile");
} catch (error) {
const errorString = JSON.stringify(error)
console.log(error)
res.status(400).json({ errorString })
const errorString = JSON.stringify(error);
console.log(error);
res.status(400).json({ errorString });
}
} else {
// Handle any other HTTP method
}
}
export default withSession(handler)
export default withSession(handler);

View file

@ -1,51 +1,51 @@
// This API route authenticates Stytch OTP codes.
import type { NextApiRequest, NextApiResponse } from 'next'
import { Session } from 'next-iron-session'
import withSession from '../../lib/withSession'
import loadStytch from '../../lib/loadStytch'
type NextIronRequest = NextApiRequest & { session: Session }
import type { NextApiRequest, NextApiResponse } from "next";
import { Session } from "next-iron-session";
import withSession from "../../lib/withSession";
import loadStytch from "../../lib/loadStytch";
type NextIronRequest = NextApiRequest & { session: Session };
type Data = {
msg: string
}
msg: string;
};
export async function handler(
req: NextIronRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
if (req.method === 'POST') {
const client = loadStytch()
const data = JSON.parse(req.body)
if (req.method === "POST") {
const client = loadStytch();
const data = JSON.parse(req.body);
try {
// params are of type stytch.LoginOrCreateUserBySMSRequest
const params = {
code: data.otpInput,
method_id: data.methodId,
}
};
const resp = await client.otps.authenticate(params)
if (resp.status_code.toString() === '200') {
const resp = await client.otps.authenticate(params);
if (resp.status_code.toString() === "200") {
// Set session
req.session.destroy()
req.session.destroy();
// Save additional user data here
req.session.set('user', {
req.session.set("user", {
id: resp.user_id,
})
await req.session.save()
});
await req.session.save();
res
.status(200)
.send({ msg: `successfully authenticated ${resp.user_id}` })
.send({ msg: `successfully authenticated ${resp.user_id}` });
} else {
throw Error('Error authenticating your code')
throw Error("Error authenticating your code");
}
} catch (error) {
const errorString = JSON.stringify(error)
console.log(error)
res.status(400).json({ msg: errorString })
const errorString = JSON.stringify(error);
console.log(error);
res.status(400).json({ msg: errorString });
}
} else {
// Handle any other HTTP method
}
}
export default withSession(handler)
export default withSession(handler);

View file

@ -1,30 +1,30 @@
// This API route logs a user out.
import type { NextApiRequest, NextApiResponse } from 'next'
import { Session } from 'next-iron-session'
import withSession from '../../lib/withSession'
type NextIronRequest = NextApiRequest & { session: Session }
import type { NextApiRequest, NextApiResponse } from "next";
import { Session } from "next-iron-session";
import withSession from "../../lib/withSession";
type NextIronRequest = NextApiRequest & { session: Session };
type Data = {
errorString: string
}
errorString: string;
};
export async function handler(
req: NextIronRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
if (req.method === 'POST') {
if (req.method === "POST") {
try {
// Set session
req.session.destroy()
res.redirect('/')
req.session.destroy();
res.redirect("/");
} catch (error) {
const errorString = JSON.stringify(error)
console.log(error)
res.status(400).json({ errorString })
const errorString = JSON.stringify(error);
console.log(error);
res.status(400).json({ errorString });
}
} else {
// Handle any other HTTP method
}
}
export default withSession(handler)
export default withSession(handler);

View file

@ -1,32 +1,32 @@
// This API route sends an OTP code to a specified number.
import type { NextApiRequest, NextApiResponse } from 'next'
import loadStytch from '../../lib/loadStytch'
import type { NextApiRequest, NextApiResponse } from "next";
import loadStytch from "../../lib/loadStytch";
type Data = {
methodId: string
}
methodId: string;
};
export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
if (req.method === 'POST') {
const client = loadStytch()
const data = JSON.parse(req.body)
if (req.method === "POST") {
const client = loadStytch();
const data = JSON.parse(req.body);
try {
const phoneNumber = data.phoneNumber.replace(/\D/g, '')
const phoneNumber = data.phoneNumber.replace(/\D/g, "");
// params are of type stytch.LoginOrCreateUserBySMSRequest
const params = {
phone_number: `${data.intlCode}${phoneNumber}`,
}
};
const resp = await client.otps.sms.loginOrCreate(params)
res.status(200).json({ methodId: resp.phone_id })
const resp = await client.otps.sms.loginOrCreate(params);
res.status(200).json({ methodId: resp.phone_id });
} catch (error) {
console.log(error)
res.status(400)
console.log(error);
res.status(400);
}
} else {
// Handle any other HTTP method
}
}
export default handler
export default handler;

View file

@ -1,34 +1,34 @@
import React, { useEffect } from 'react'
import { useRouter } from 'next/router'
import { Stytch, StytchProps } from '@stytch/stytch-react'
import { OAuthProvidersTypes, SDKProductTypes } from '@stytch/stytch-js'
import styles from '../styles/Home.module.css'
import withSession, { ServerSideProps } from '../lib/withSession'
import LoginWithSMS from '../components/LoginWithSMS'
import { LoginMethod } from '../lib/types'
import LoginEntryPoint from '../components/LoginEntryPoint'
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import { Stytch, StytchProps } from "@stytch/stytch-react";
import { OAuthProvidersTypes, SDKProductTypes } from "@stytch/stytch-js";
import styles from "../styles/Home.module.css";
import withSession, { ServerSideProps } from "../lib/withSession";
import LoginWithSMS from "../components/LoginWithSMS";
import { LoginMethod } from "../lib/types";
import LoginEntryPoint from "../components/LoginEntryPoint";
// Set the URL base for redirect URLs. The three cases are as follows:
// 1. Running locally via `vercel dev`; VERCEL_URL will contain localhost, but will not be https.
// 2. Deploying via Vercel; VERCEL_URL will be generated on runtime and use https.
// 3. Running locally via `npm run dev`; VERCEL_URL will be undefined and the app will be at localhost.
let REDIRECT_URL_BASE = ''
let REDIRECT_URL_BASE = "";
if (process.env.NEXT_PUBLIC_VERCEL_URL?.includes('localhost')) {
REDIRECT_URL_BASE = 'http://localhost:3000'
if (process.env.NEXT_PUBLIC_VERCEL_URL?.includes("localhost")) {
REDIRECT_URL_BASE = "http://localhost:3000";
} else if (process.env.NEXT_PUBLIC_VERCEL_URL !== undefined) {
REDIRECT_URL_BASE = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
REDIRECT_URL_BASE = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
} else {
REDIRECT_URL_BASE = 'http://localhost:3000'
REDIRECT_URL_BASE = "http://localhost:3000";
}
const stytchProps: StytchProps = {
loginOrSignupView: {
products: [SDKProductTypes.oauth, SDKProductTypes.emailMagicLinks],
emailMagicLinksOptions: {
loginRedirectURL: REDIRECT_URL_BASE + '/api/authenticate_magic_link',
loginRedirectURL: REDIRECT_URL_BASE + "/api/authenticate_magic_link",
loginExpirationMinutes: 30,
signupRedirectURL: REDIRECT_URL_BASE + '/api/authenticate_magic_link',
signupRedirectURL: REDIRECT_URL_BASE + "/api/authenticate_magic_link",
signupExpirationMinutes: 30,
createUserAsPending: false,
},
@ -42,11 +42,11 @@ const stytchProps: StytchProps = {
},
style: {
fontFamily: '"Helvetica New", Helvetica, sans-serif',
primaryColor: '#0577CA',
primaryTextColor: '#090909',
width: '321px',
primaryColor: "#0577CA",
primaryTextColor: "#090909",
width: "321px",
},
publicToken: process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN || '',
publicToken: process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN || "",
callbacks: {
onEvent: (data: { eventData: { userId: any; email: any } }) => {
if (
@ -56,47 +56,49 @@ const stytchProps: StytchProps = {
console.log({
userId: data.eventData.userId,
email: data.eventData.email,
})
});
} else {
console.warn('The user is not found. Data: ', data)
console.warn("The user is not found. Data: ", data);
}
},
onSuccess: (data) => console.log(data),
onError: (data) => console.log(data),
},
}
};
type Props = {
publicToken: string
publicToken: string;
user: {
id: string
}
}
id: string;
};
};
const App = (props: Props) => {
const { user, publicToken } = props
const [loginMethod, setLoginMethod] = React.useState<LoginMethod | null>(null)
const router = useRouter()
const { user, publicToken } = props;
const [loginMethod, setLoginMethod] = React.useState<LoginMethod | null>(
null,
);
const router = useRouter();
useEffect(() => {
if (user) {
router.push('/profile')
router.push("/profile");
}
})
});
const loginMethodMap: Record<LoginMethod, React.ReactElement> = {
[LoginMethod.API]: <LoginWithSMS />,
[LoginMethod.SDK]: (
<div className={styles.container}>
<Stytch
publicToken={publicToken || ''}
publicToken={publicToken || ""}
loginOrSignupView={stytchProps.loginOrSignupView}
style={stytchProps.style}
callbacks={stytchProps.callbacks}
/>
</div>
),
}
};
return (
<div className={styles.root}>
@ -106,19 +108,19 @@ const App = (props: Props) => {
loginMethodMap[loginMethod]
)}
</div>
)
}
);
};
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
// Get the user's session based on the request
const user = req.session.get('user') ?? null
const user = req.session.get("user") ?? null;
const props: Props = {
publicToken: stytchProps.publicToken,
user,
}
return { props }
}
};
return { props };
};
export const getServerSideProps = withSession(getServerSidePropsHandler)
export const getServerSideProps = withSession(getServerSidePropsHandler);
export default App
export default App;

View file

@ -1,31 +1,31 @@
import React, { useEffect } from 'react'
import styles from '../styles/Home.module.css'
import StytchContainer from '../components/StytchContainer'
import withSession, { ServerSideProps } from '../lib/withSession'
import { useRouter } from 'next/router'
import React, { useEffect } from "react";
import styles from "../styles/Home.module.css";
import StytchContainer from "../components/StytchContainer";
import withSession, { ServerSideProps } from "../lib/withSession";
import { useRouter } from "next/router";
type Props = {
user?: {
id: string
}
}
id: string;
};
};
const Profile = (props: Props) => {
const { user } = props
const router = useRouter()
const { user } = props;
const router = useRouter();
useEffect(() => {
if (!user) {
router.replace('/')
router.replace("/");
}
})
});
const signOut = async () => {
const resp = await fetch('/api/logout', { method: 'POST' })
const resp = await fetch("/api/logout", { method: "POST" });
if (resp.status === 200) {
router.push('/')
router.push("/");
}
}
};
return (
<>
@ -33,12 +33,12 @@ const Profile = (props: Props) => {
<div />
) : (
<StytchContainer>
<h2>{'Welcome!'}</h2>
<h2>{"Welcome!"}</h2>
<p className={styles.profileSubHeader}>
Thank you for using Stytch! Heres your user info.
</p>
<pre className={styles.code}>
{JSON.stringify(user, null, 1).replace(' ', '')}
{JSON.stringify(user, null, 1).replace(" ", "")}
</pre>
<button className={styles.primaryButton} onClick={signOut}>
Sign out
@ -46,16 +46,16 @@ const Profile = (props: Props) => {
</StytchContainer>
)}
</>
)
}
);
};
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
// Get the user's session based on the request
const user = req.session.get('user') ?? null
const props: Props = { user }
return { props }
}
const user = req.session.get("user") ?? null;
const props: Props = { user };
return { props };
};
export const getServerSideProps = withSession(getServerSidePropsHandler)
export const getServerSideProps = withSession(getServerSidePropsHandler);
export default Profile
export default Profile;

Some files were not shown because too many files have changed in this diff Show more