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

View file

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

View file

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

View file

@ -1,14 +1,14 @@
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import Nav from '../components/Nav' import Nav from "../components/Nav";
const SlugPage = () => { const SlugPage = () => {
const { asPath } = useRouter() const { asPath } = useRouter();
return ( return (
<> <>
<Nav /> <Nav />
<p>Hello, I'm the {asPath} page</p> <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 = () => ( const AboutPage = () => (
<> <>
<Nav /> <Nav />
<p>Hello, I'm the about page</p> <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 = () => ( const IndexPage = () => (
<> <>
<Nav /> <Nav />
<p>Hello, I'm the index page</p> <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 = () => ( const News = () => (
<> <>
<Nav /> <Nav />
<p>Hello, I'm the news page</p> <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 // Only the intrinsic elements defined here will be accepted, and only with the attributes defined here
declare namespace JSX { declare namespace JSX {
interface AmpImg { interface AmpImg {
alt?: string alt?: string;
src?: string src?: string;
width?: string | number width?: string | number;
height?: string | number height?: string | number;
layout?: string layout?: string;
fallback?: string fallback?: string;
children?: React.ReactNode children?: React.ReactNode;
} }
interface IntrinsicElements { interface IntrinsicElements {
'amp-img': AmpImg "amp-img": AmpImg;
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,14 +1,14 @@
import Head from 'next/head' import Head from "next/head";
import { useAmp } from 'next/amp' import { useAmp } from "next/amp";
import Layout from '../components/Layout' import Layout from "../components/Layout";
import Byline from '../components/Byline' import Byline from "../components/Byline";
export const config = { export const config = {
amp: true, amp: true,
} };
export default function IndexPage() { export default function IndexPage() {
const isAmp = useAmp() const isAmp = useAmp();
return ( return (
<Layout> <Layout>
@ -35,7 +35,7 @@ export default function IndexPage() {
></amp-img> ></amp-img>
</amp-img> </amp-img>
<p> <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 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, yourself 4 hours - checked, have your beauty sleep 18 hours - checked,
be fabulous for the rest of the day - checked!. Hide from vacuum be fabulous for the rest of the day - checked!. Hide from vacuum
@ -233,5 +233,5 @@ export default function IndexPage() {
} }
`}</style> `}</style>
</Layout> </Layout>
) );
} }

View file

@ -1,3 +1,3 @@
export default function NormalPage() { 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')({ const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === 'true', enabled: process.env.ANALYZE === "true",
}) });
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// any configs you need // any configs you need
} };
module.exports = withBundleAnalyzer(nextConfig) module.exports = withBundleAnalyzer(nextConfig);

View file

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

View file

@ -1,54 +1,54 @@
import { useMemo } from 'react' import { useMemo } from "react";
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client' import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { SchemaLink } from '@apollo/client/link/schema' import { SchemaLink } from "@apollo/client/link/schema";
import { schema } from '../apollo/schema' import { schema } from "../apollo/schema";
import merge from 'deepmerge' import merge from "deepmerge";
let apolloClient let apolloClient;
function createIsomorphLink() { function createIsomorphLink() {
if (typeof window === 'undefined') { if (typeof window === "undefined") {
return new SchemaLink({ schema }) return new SchemaLink({ schema });
} else { } else {
return new HttpLink({ return new HttpLink({
uri: '/api/graphql', uri: "/api/graphql",
credentials: 'same-origin', credentials: "same-origin",
}) });
} }
} }
function createApolloClient() { function createApolloClient() {
return new ApolloClient({ return new ApolloClient({
ssrMode: typeof window === 'undefined', ssrMode: typeof window === "undefined",
link: createIsomorphLink(), link: createIsomorphLink(),
cache: new InMemoryCache(), cache: new InMemoryCache(),
}) });
} }
export function initializeApollo(initialState = null) { 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 // If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here // get hydrated here
if (initialState) { if (initialState) {
// Get existing cache, loaded during client side data fetching // 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 // 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 // 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 // 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 // Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient return _apolloClient;
} }
export function useApollo(initialState) { export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState]) const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store return store;
} }

View file

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

View file

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

View file

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

View file

@ -1,17 +1,17 @@
export default function Field({ name, label, type, autoComplete, required }) { export default function Field({ name, label, type, autoComplete, required }) {
return ( return (
<div> <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} {required ? <span title="Required">*</span> : undefined}
</label> </label>
<br /> <br />
<input <input
autoComplete={autoComplete} autoComplete={autoComplete}
id={[name, 'input'].join('-')} id={[name, "input"].join("-")}
name={name} name={name}
required={required} required={required}
type={type} type={type}
/> />
</div> </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) { export function setTokenCookie(res, token) {
const cookie = serialize(TOKEN_NAME, token, { const cookie = serialize(TOKEN_NAME, token, {
maxAge: MAX_AGE, maxAge: MAX_AGE,
expires: new Date(Date.now() + MAX_AGE * 1000), expires: new Date(Date.now() + MAX_AGE * 1000),
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === "production",
path: '/', path: "/",
sameSite: 'lax', sameSite: "lax",
}) });
res.setHeader('Set-Cookie', cookie) res.setHeader("Set-Cookie", cookie);
} }
export function removeTokenCookie(res) { export function removeTokenCookie(res) {
const cookie = serialize(TOKEN_NAME, '', { const cookie = serialize(TOKEN_NAME, "", {
maxAge: -1, maxAge: -1,
path: '/', path: "/",
}) });
res.setHeader('Set-Cookie', cookie) res.setHeader("Set-Cookie", cookie);
} }
export function parseCookies(req) { export function parseCookies(req) {
// For API Routes we don't need to parse the cookies. // 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. // For pages we do need to parse the cookies.
const cookie = req.headers?.cookie const cookie = req.headers?.cookie;
return parse(cookie || '') return parse(cookie || "");
} }
export function getTokenCookie(req) { export function getTokenCookie(req) {
const cookies = parseCookies(req) const cookies = parseCookies(req);
return cookies[TOKEN_NAME] return cookies[TOKEN_NAME];
} }

View file

@ -1,29 +1,29 @@
import Iron from '@hapi/iron' import Iron from "@hapi/iron";
import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies' 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) { 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 // Create a session object with a max age that we can validate later
const obj = { ...session, createdAt, maxAge: MAX_AGE } const obj = { ...session, createdAt, maxAge: MAX_AGE };
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults) const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
setTokenCookie(res, token) setTokenCookie(res, token);
} }
export async function getLoginSession(req) { 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 session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
const expiresAt = session.createdAt + session.maxAge * 1000 const expiresAt = session.createdAt + session.maxAge * 1000;
// Validate the expiration date of the session // Validate the expiration date of the session
if (Date.now() > expiresAt) { 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) { for (const graphQLError of error.graphQLErrors) {
if ( if (
graphQLError.extensions && 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 crypto from "crypto";
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from "uuid";
/** /**
* User methods. The example doesn't contain a DB, but for real applications you must use a * 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. * db here, such as MongoDB, Fauna, SQL, etc.
*/ */
const users = [] const users = [];
export async function createUser({ email, password }) { export async function createUser({ email, password }) {
// Here you should create the user and save the salt and hashed password (some dbs may have // 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): // 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 const hash = crypto
.pbkdf2Sync(password, salt, 1000, 64, 'sha512') .pbkdf2Sync(password, salt, 1000, 64, "sha512")
.toString('hex') .toString("hex");
const user = { const user = {
id: uuidv4(), id: uuidv4(),
createdAt: Date.now(), createdAt: Date.now(),
email, email,
hash, hash,
salt, salt,
} };
// This is an in memory store for users, there is no data persistence without a proper DB // 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 // Here you should lookup for the user in your DB
export async function findUser({ email }) { export async function findUser({ email }) {
// This is an in memory store for users, there is no data persistence without a proper DB // 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 // Compare the password of an already fetched user (using `findUser`) and compare the
// password for a potential match // password for a potential match
export async function validatePassword(user, inputPassword) { export async function validatePassword(user, inputPassword) {
const inputHash = crypto const inputHash = crypto
.pbkdf2Sync(inputPassword, user.salt, 1000, 64, 'sha512') .pbkdf2Sync(inputPassword, user.salt, 1000, 64, "sha512")
.toString('hex') .toString("hex");
const passwordsMatch = user.hash === inputHash const passwordsMatch = user.hash === inputHash;
return passwordsMatch return passwordsMatch;
} }

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react' import { useEffect } from "react";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import Link from 'next/link' import Link from "next/link";
import { gql, useQuery } from '@apollo/client' import { gql, useQuery } from "@apollo/client";
const ViewerQuery = gql` const ViewerQuery = gql`
query ViewerQuery { query ViewerQuery {
@ -10,36 +10,36 @@ const ViewerQuery = gql`
email email
} }
} }
` `;
const Index = () => { const Index = () => {
const router = useRouter() const router = useRouter();
const { data, loading, error } = useQuery(ViewerQuery) const { data, loading, error } = useQuery(ViewerQuery);
const viewer = data?.viewer const viewer = data?.viewer;
const shouldRedirect = !(loading || error || viewer) const shouldRedirect = !(loading || error || viewer);
useEffect(() => { useEffect(() => {
if (shouldRedirect) { if (shouldRedirect) {
router.push('/signin') router.push("/signin");
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldRedirect]) }, [shouldRedirect]);
if (error) { if (error) {
return <p>{error.message}</p> return <p>{error.message}</p>;
} }
if (viewer) { if (viewer) {
return ( return (
<div> <div>
You're signed in as {viewer.email}. Go to{' '} You're signed in as {viewer.email}. Go to{" "}
<Link href="/about">about</Link> page or{' '} <Link href="/about">about</Link> page or{" "}
<Link href="/signout">signout</Link>. <Link href="/signout">signout</Link>.
</div> </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 { useState } from "react";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import Link from 'next/link' import Link from "next/link";
import { gql } from '@apollo/client' import { gql } from "@apollo/client";
import { useMutation, useApolloClient } from '@apollo/client' import { useMutation, useApolloClient } from "@apollo/client";
import { getErrorMessage } from '../lib/form' import { getErrorMessage } from "../lib/form";
import Field from '../components/field' import Field from "../components/field";
const SignInMutation = gql` const SignInMutation = gql`
mutation SignInMutation($email: String!, $password: String!) { mutation SignInMutation($email: String!, $password: String!) {
@ -15,33 +15,33 @@ const SignInMutation = gql`
} }
} }
} }
` `;
function SignIn() { function SignIn() {
const client = useApolloClient() const client = useApolloClient();
const [signIn] = useMutation(SignInMutation) const [signIn] = useMutation(SignInMutation);
const [errorMsg, setErrorMsg] = useState() const [errorMsg, setErrorMsg] = useState();
const router = useRouter() const router = useRouter();
async function handleSubmit(event) { async function handleSubmit(event) {
event.preventDefault() event.preventDefault();
const emailElement = event.currentTarget.elements.email const emailElement = event.currentTarget.elements.email;
const passwordElement = event.currentTarget.elements.password const passwordElement = event.currentTarget.elements.password;
try { try {
await client.resetStore() await client.resetStore();
const { data } = await signIn({ const { data } = await signIn({
variables: { variables: {
email: emailElement.value, email: emailElement.value,
password: passwordElement.value, password: passwordElement.value,
}, },
}) });
if (data.signIn.user) { if (data.signIn.user) {
await router.push('/') await router.push("/");
} }
} catch (error) { } catch (error) {
setErrorMsg(getErrorMessage(error)) setErrorMsg(getErrorMessage(error));
} }
} }
@ -64,11 +64,11 @@ function SignIn() {
required required
label="Password" label="Password"
/> />
<button type="submit">Sign in</button> or{' '} <button type="submit">Sign in</button> or{" "}
<Link href="/signup">Sign up</Link> <Link href="/signup">Sign up</Link>
</form> </form>
</> </>
) );
} }
export default SignIn export default SignIn;

View file

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

View file

@ -1,54 +1,54 @@
import { useMemo } from 'react' import { useMemo } from "react";
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client' import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { SchemaLink } from '@apollo/client/link/schema' import { SchemaLink } from "@apollo/client/link/schema";
import { schema } from '../apollo/schema' import { schema } from "../apollo/schema";
import merge from 'deepmerge' import merge from "deepmerge";
let apolloClient let apolloClient;
function createIsomorphLink() { function createIsomorphLink() {
if (typeof window === 'undefined') { if (typeof window === "undefined") {
return new SchemaLink({ schema }) return new SchemaLink({ schema });
} else { } else {
return new HttpLink({ return new HttpLink({
uri: '/api/graphql', uri: "/api/graphql",
credentials: 'same-origin', credentials: "same-origin",
}) });
} }
} }
function createApolloClient() { function createApolloClient() {
return new ApolloClient({ return new ApolloClient({
ssrMode: typeof window === 'undefined', ssrMode: typeof window === "undefined",
link: createIsomorphLink(), link: createIsomorphLink(),
cache: new InMemoryCache(), cache: new InMemoryCache(),
}) });
} }
export function initializeApollo(initialState = null) { 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 // If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here // gets hydrated here
if (initialState) { if (initialState) {
// Get existing cache, loaded during client side data fetching // 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 // 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 // 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 // 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 // Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient return _apolloClient;
} }
export function useApollo(initialState) { export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState]) const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store return store;
} }

View file

@ -1,7 +1,7 @@
export const resolvers = { export const resolvers = {
Query: { Query: {
viewer() { 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 { makeExecutableSchema } from "@graphql-tools/schema";
import { typeDefs } from './type-defs' import { typeDefs } from "./type-defs";
import { resolvers } from './resolvers' import { resolvers } from "./resolvers";
export const schema = makeExecutableSchema({ export const schema = makeExecutableSchema({
typeDefs, typeDefs,
resolvers, resolvers,
}) });

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { ApolloServer } from '@apollo/server' import { ApolloServer } from "@apollo/server";
import { startServerAndCreateNextHandler } from '@as-integrations/next' import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { schema } from '../../apollo/schema' 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 gql from "graphql-tag";
import Link from 'next/link' import Link from "next/link";
import { useQuery } from '@apollo/client' import { useQuery } from "@apollo/client";
import { initializeApollo } from '../apollo/client' import { initializeApollo } from "../apollo/client";
const ViewerQuery = gql` const ViewerQuery = gql`
query ViewerQuery { query ViewerQuery {
@ -11,33 +11,33 @@ const ViewerQuery = gql`
status status
} }
} }
` `;
const Index = () => { const Index = () => {
const { const {
data: { viewer }, data: { viewer },
} = useQuery(ViewerQuery) } = useQuery(ViewerQuery);
return ( return (
<div> <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. <Link href="/about">static</Link> page.
</div> </div>
) );
} };
export async function getStaticProps() { export async function getStaticProps() {
const apolloClient = initializeApollo() const apolloClient = initializeApollo();
await apolloClient.query({ await apolloClient.query({
query: ViewerQuery, query: ViewerQuery,
}) });
return { return {
props: { props: {
initialApolloState: apolloClient.cache.extract(), 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 }) { export default function UserProfile({ user }) {
if (!user) { if (!user) {
return <h1>User Not Found</h1> return <h1>User Not Found</h1>;
} }
return ( return (
<h1> <h1>
{user.username} is {user.name} {user.username} is {user.name}
</h1> </h1>
) );
} }
export async function getStaticProps(context) { export async function getStaticProps(context) {
const { params } = context const { params } = context;
const { username } = params const { username } = params;
const { user = null } = await queryGraphql( const { user = null } = await queryGraphql(
` `
query($username: String) { query($username: String) {
@ -23,9 +23,9 @@ export async function getStaticProps(context) {
} }
} }
`, `,
{ username } { username },
) );
return { props: { user } } return { props: { user } };
} }
export async function getStaticPaths() { export async function getStaticPaths() {
@ -35,12 +35,12 @@ export async function getStaticPaths() {
username username
} }
} }
`)) as { users: { username: string }[] } `)) as { users: { username: string }[] };
return { return {
paths: users.map(({ username }) => ({ paths: users.map(({ username }) => ({
params: { username }, params: { username },
})), })),
fallback: true, fallback: true,
} };
} }

View file

@ -1,7 +1,7 @@
import { ApolloServer } from '@apollo/server' import { ApolloServer } from "@apollo/server";
import { startServerAndCreateNextHandler } from '@as-integrations/next' import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { makeExecutableSchema } from '@graphql-tools/schema' import { makeExecutableSchema } from "@graphql-tools/schema";
import { gql } from 'graphql-tag' import { gql } from "graphql-tag";
const typeDefs = gql` const typeDefs = gql`
type Query { type Query {
@ -12,28 +12,28 @@ const typeDefs = gql`
name: String name: String
username: String username: String
} }
` `;
const users = [ const users = [
{ name: 'Leeroy Jenkins', username: 'leeroy' }, { name: "Leeroy Jenkins", username: "leeroy" },
{ name: 'Foo Bar', username: 'foobar' }, { name: "Foo Bar", username: "foobar" },
] ];
const resolvers = { const resolvers = {
Query: { Query: {
users() { users() {
return users return users;
}, },
user(parent, { username }) { 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({ const server = new ApolloServer({
schema, 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 }) { export default function UserListing({ users }) {
return ( return (
@ -16,7 +16,7 @@ export default function UserListing({ users }) {
))} ))}
</ul> </ul>
</div> </div>
) );
} }
export async function getStaticProps() { export async function getStaticProps() {
@ -27,6 +27,6 @@ export async function getStaticProps() {
username 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 = {}) { export default async function queryGraphql(query, variableValues = {}) {
const { data } = await graphql({ schema, source: query, variableValues }) const { data } = await graphql({ schema, source: query, variableValues });
return data || {} return data || {};
} }

View file

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

View file

@ -1,4 +1,4 @@
import { createYoga, createSchema } from 'graphql-yoga' import { createYoga, createSchema } from "graphql-yoga";
const typeDefs = /* GraphQL */ ` const typeDefs = /* GraphQL */ `
type Query { type Query {
@ -7,30 +7,30 @@ const typeDefs = /* GraphQL */ `
type User { type User {
name: String name: String
} }
` `;
const resolvers = { const resolvers = {
Query: { Query: {
users() { users() {
return [{ name: 'Nextjs' }] return [{ name: "Nextjs" }];
}, },
}, },
} };
const schema = createSchema({ const schema = createSchema({
typeDefs, typeDefs,
resolvers, resolvers,
}) });
export const config = { export const config = {
api: { api: {
// Disable body parsing (required for file uploads) // Disable body parsing (required for file uploads)
bodyParser: false, bodyParser: false,
}, },
} };
export default createYoga({ export default createYoga({
schema, schema,
// Needed to be defined explicitly because our endpoint lives at a different path other than `/graphql` // 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) => const fetcher = (query: string) =>
fetch('/api/graphql', { fetch("/api/graphql", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-type': 'application/json', "Content-type": "application/json",
}, },
body: JSON.stringify({ query }), body: JSON.stringify({ query }),
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((json) => json.data) .then((json) => json.data);
type Data = { type Data = {
users: { users: {
name: string name: string;
}[] }[];
} };
export default function Index() { 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 (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div> if (isLoading) return <div>Loading...</div>;
if (!data) return null if (!data) return null;
const { users } = data const { users } = data;
return ( return (
<div> <div>
@ -32,5 +35,5 @@ export default function Index() {
<div key={index}>{user.name}</div> <div key={index}>{user.name}</div>
))} ))}
</div> </div>
) );
} }

View file

@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import { setCookie } from '../../utils/cookies' import { setCookie } from "../../utils/cookies";
export default function handler(_req: NextApiRequest, res: NextApiResponse) { export default function handler(_req: NextApiRequest, res: NextApiResponse) {
// Calling our pure function using the `res` object, it will add the `set-cookie` header // 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! // 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() { 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 (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div> if (isLoading) return <div>Loading...</div>;
if (!data) return null 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 type { NextApiResponse } from "next";
import { serialize, CookieSerializeOptions } from 'cookie' import { serialize, CookieSerializeOptions } from "cookie";
/** /**
* This sets `cookie` using the `res` object * This sets `cookie` using the `res` object
@ -9,14 +9,14 @@ export const setCookie = (
res: NextApiResponse, res: NextApiResponse,
name: string, name: string,
value: unknown, value: unknown,
options: CookieSerializeOptions = {} options: CookieSerializeOptions = {},
) => { ) => {
const stringValue = 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') { if (typeof options.maxAge === "number") {
options.expires = new Date(Date.now() + options.maxAge * 1000) 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 type { NextApiRequest, NextApiResponse } from "next";
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from "uuid";
import rateLimit from '../../utils/rate-limit' import rateLimit from "../../utils/rate-limit";
const limiter = rateLimit({ const limiter = rateLimit({
interval: 60 * 1000, // 60 seconds interval: 60 * 1000, // 60 seconds
uniqueTokenPerInterval: 500, // Max 500 users per second uniqueTokenPerInterval: 500, // Max 500 users per second
}) });
export default async function handler( export default async function handler(
_req: NextApiRequest, _req: NextApiRequest,
res: NextApiResponse res: NextApiResponse,
) { ) {
try { try {
await limiter.check(res, 10, 'CACHE_TOKEN') // 10 requests per minute await limiter.check(res, 10, "CACHE_TOKEN"); // 10 requests per minute
res.status(200).json({ id: uuidv4() }) res.status(200).json({ id: uuidv4() });
} catch { } 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 { useState } from "react";
import styles from '../styles.module.css' import styles from "../styles.module.css";
export default function Index() { 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 makeRequest = async () => {
const res = await fetch('/api/user') const res = await fetch("/api/user");
setResponse({ setResponse({
status: res.status, status: res.status,
body: await res.json(), body: await res.json(),
limit: res.headers.get('X-RateLimit-Limit'), limit: res.headers.get("X-RateLimit-Limit"),
remaining: res.headers.get('X-RateLimit-Remaining'), remaining: res.headers.get("X-RateLimit-Remaining"),
}) });
} };
return ( return (
<main className={styles.container}> <main className={styles.container}>
<h1>Next.js API Routes Rate Limiting</h1> <h1>Next.js API Routes Rate Limiting</h1>
<p> <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 to implement a simple rate limiter for API routes (Serverless
Functions). Functions).
</p> </p>
@ -30,5 +32,5 @@ export default function Index() {
</code> </code>
)} )}
</main> </main>
) );
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import type { User } from '../../interfaces' import type { User } from "../../interfaces";
// Fake users data // 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( export default function handler(
_req: NextApiRequest, _req: NextApiRequest,
res: NextApiResponse<User[]> res: NextApiResponse<User[]>,
) { ) {
// Get data from your database // 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 type { User } from "../interfaces";
import useSwr from 'swr' import useSwr from "swr";
import Link from 'next/link' 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() { 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 (error) return <div>Failed to load users</div>;
if (isLoading) return <div>Loading...</div> if (isLoading) return <div>Loading...</div>;
if (!data) return null if (!data) return null;
return ( return (
<ul> <ul>
@ -21,5 +21,5 @@ export default function Index() {
</li> </li>
))} ))}
</ul> </ul>
) );
} }

View file

@ -1,19 +1,19 @@
import type { User } from '../../interfaces' import type { User } from "../../interfaces";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import useSwr from 'swr' 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() { export default function UserPage() {
const { query } = useRouter() const { query } = useRouter();
const { data, error, isLoading } = useSwr<User>( const { data, error, isLoading } = useSwr<User>(
query.id ? `/api/user/${query.id}` : null, query.id ? `/api/user/${query.id}` : null,
fetcher fetcher,
) );
if (error) return <div>Failed to load user</div> if (error) return <div>Failed to load user</div>;
if (isLoading) return <div>Loading...</div> if (isLoading) return <div>Loading...</div>;
if (!data) return null 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 Link from "next/link";
import { Person } from '../interfaces' import { Person } from "../interfaces";
type PersonProps = { type PersonProps = {
person: Person person: Person;
} };
export default function PersonComponent({ person }: PersonProps) { export default function PersonComponent({ person }: PersonProps) {
return ( return (
@ -12,5 +12,5 @@ export default function PersonComponent({ person }: PersonProps) {
{person.name} {person.name}
</Link> </Link>
</li> </li>
) );
} }

View file

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

View file

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

View file

@ -1,17 +1,17 @@
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from "next";
import { people } from '../../../data' import { people } from "../../../data";
import type { Person, ResponseError } from '../../../interfaces' import type { Person, ResponseError } from "../../../interfaces";
export default function personHandler( export default function personHandler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Person | ResponseError> res: NextApiResponse<Person | ResponseError>,
) { ) {
const { query } = req const { query } = req;
const { id } = query const { id } = query;
const person = people.find((p) => p.id === id) const person = people.find((p) => p.id === id);
// User with id exists // User with id exists
return person return person
? res.status(200).json(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 { NextApiResponse, NextApiRequest } from "next";
import { people } from '../../../data' import { people } from "../../../data";
import { Person } from '../../../interfaces' import { Person } from "../../../interfaces";
export default function handler( export default function handler(
_req: NextApiRequest, _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 useSWR from "swr";
import PersonComponent from '../components/Person' import PersonComponent from "../components/Person";
import type { Person } from '../interfaces' 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() { 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 (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div> if (isLoading) return <div>Loading...</div>;
if (!data) return null if (!data) return null;
return ( return (
<ul> <ul>
@ -17,5 +17,5 @@ export default function Index() {
<PersonComponent key={p.id} person={p} /> <PersonComponent key={p.id} person={p} />
))} ))}
</ul> </ul>
) );
} }

View file

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

View file

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

View file

@ -1,17 +1,17 @@
'use client' "use client";
import { usePathname } from 'next/navigation' import { usePathname } from "next/navigation";
import Link from 'next/link' import Link from "next/link";
import { i18n, type Locale } from '../../../i18n-config' import { i18n, type Locale } from "../../../i18n-config";
export default function LocaleSwitcher() { export default function LocaleSwitcher() {
const pathName = usePathname() const pathName = usePathname();
const redirectedPathName = (locale: Locale) => { const redirectedPathName = (locale: Locale) => {
if (!pathName) return '/' if (!pathName) return "/";
const segments = pathName.split('/') const segments = pathName.split("/");
segments[1] = locale segments[1] = locale;
return segments.join('/') return segments.join("/");
} };
return ( return (
<div> <div>
@ -22,9 +22,9 @@ export default function LocaleSwitcher() {
<li key={locale}> <li key={locale}>
<Link href={redirectedPathName(locale)}>{locale}</Link> <Link href={redirectedPathName(locale)}>{locale}</Link>
</li> </li>
) );
})} })}
</ul> </ul>
</div> </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() { export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale })) return i18n.locales.map((locale) => ({ lang: locale }));
} }
export default function Root({ export default function Root({
children, children,
params, params,
}: { }: {
children: React.ReactNode children: React.ReactNode;
params: { lang: Locale } params: { lang: Locale };
}) { }) {
return ( return (
<html lang={params.lang}> <html lang={params.lang}>
<body>{children}</body> <body>{children}</body>
</html> </html>
) );
} }
export const metadata = { export const metadata = {
title: 'i18n within app directory - Vercel Examples', title: "i18n within app directory - Vercel Examples",
description: 'How to do i18n in Next.js 13 within app directory', description: "How to do i18n in Next.js 13 within app directory",
} };

View file

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

View file

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

View file

@ -1,6 +1,6 @@
export const i18n = { export const i18n = {
defaultLocale: 'en', defaultLocale: "en",
locales: ['en', 'de', 'cs'], locales: ["en", "de", "cs"],
} as const } 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 { NextResponse } from "next/server";
import type { NextRequest } 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 { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from 'negotiator' import Negotiator from "negotiator";
function getLocale(request: NextRequest): string | undefined { function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers // Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {} const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value)) request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// @ts-ignore locales are readonly // @ts-ignore locales are readonly
const locales: string[] = i18n.locales const locales: string[] = i18n.locales;
// Use negotiator and intl-localematcher to get best locale // Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages( 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) { 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. // // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// // If you have one // // If you have one
@ -40,25 +40,26 @@ export function middleware(request: NextRequest) {
// Check if there is any supported locale in the pathname // Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every( const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` (locale) =>
) !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
);
// Redirect if there is no locale // Redirect if there is no locale
if (pathnameIsMissingLocale) { if (pathnameIsMissingLocale) {
const locale = getLocale(request) const locale = getLocale(request);
// e.g. incoming request is /products // e.g. incoming request is /products
// The new URL is now /en-US/products // The new URL is now /en-US/products
return NextResponse.redirect( return NextResponse.redirect(
new URL( new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
request.url request.url,
) ),
) );
} }
} }
export const config = { export const config = {
// Matcher ignoring `/_next/` and `/api/` // 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 { :root {
--max-width: 1100px; --max-width: 1100px;
--border-radius: 12px; --border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
--foreground-rgb: 0, 0, 0; --foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220; --background-start-rgb: 214, 219, 220;

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import Image from 'next/image' import Image from "next/image";
import { Inter } from 'next/font/google' import { Inter } from "next/font/google";
import styles from './page.module.css' import styles from "./page.module.css";
import Content from './message.mdx' import Content from "./message.mdx";
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ["latin"] });
export default function Home() { export default function Home() {
return ( return (
@ -19,7 +19,7 @@ export default function Home() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
By{' '} By{" "}
<Image <Image
src="/vercel.svg" src="/vercel.svg"
alt="Vercel Logo" alt="Vercel Logo"
@ -78,5 +78,5 @@ export default function Home() {
</a> </a>
</div> </div>
</main> </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. // This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents { 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. // Allows customizing built-in components, e.g. to add styling.
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>, // h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
...components, ...components,
} };
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,28 +1,28 @@
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from "next";
import { Session, withIronSession } from 'next-iron-session' import { Session, withIronSession } from "next-iron-session";
type NextIronRequest = NextApiRequest & { session: Session } type NextIronRequest = NextApiRequest & { session: Session };
type APIHandler = ( type APIHandler = (
req: NextIronRequest, req: NextIronRequest,
res: NextApiResponse<any> res: NextApiResponse<any>,
) => Promise<any> ) => Promise<any>;
export type ServerSideProps = ({ export type ServerSideProps = ({
req, req,
}: { }: {
req: NextIronRequest req: NextIronRequest;
}) => Promise<any> }) => Promise<any>;
const withSession = (handler: APIHandler | ServerSideProps) => const withSession = (handler: APIHandler | ServerSideProps) =>
withIronSession(handler, { withIronSession(handler, {
password: process.env.IRON_SESSION_PASSWORD || '', password: process.env.IRON_SESSION_PASSWORD || "",
cookieName: process.env.IRON_SESSION_COOKIE_NAME || '', cookieName: process.env.IRON_SESSION_COOKIE_NAME || "",
// if your localhost is served on http:// then disable the secure flag // if your localhost is served on http:// then disable the secure flag
cookieOptions: { cookieOptions: {
httpOnly: true, 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/globals.css";
import styles from '../styles/Home.module.css' import styles from "../styles/Home.module.css";
import type { AppProps } from 'next/app' import type { AppProps } from "next/app";
import React from 'react' import React from "react";
import Head from 'next/head' import Head from "next/head";
import Image from 'next/image' import Image from "next/image";
import stytchLogo from '/public/stytch-logo.svg' import stytchLogo from "/public/stytch-logo.svg";
import vercelLogo from '/public/vercel-logotype-dark.svg' import vercelLogo from "/public/vercel-logotype-dark.svg";
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (
@ -50,6 +50,6 @@ function MyApp({ Component, pageProps }: AppProps) {
<Component {...pageProps} /> <Component {...pageProps} />
</div> </div>
</React.Fragment> </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() { export default function Document() {
return ( return (
@ -14,5 +14,5 @@ export default function Document() {
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
) );
} }

View file

@ -1,39 +1,39 @@
// This API route authenticates a Stytch magic link. // This API route authenticates a Stytch magic link.
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import { Session } from 'next-iron-session' import { Session } from "next-iron-session";
import withSession from '../../lib/withSession' import withSession from "../../lib/withSession";
import loadStytch from '../../lib/loadStytch' import loadStytch from "../../lib/loadStytch";
type NextIronRequest = NextApiRequest & { session: Session } type NextIronRequest = NextApiRequest & { session: Session };
type Data = { type Data = {
errorString: string errorString: string;
} };
export async function handler( export async function handler(
req: NextIronRequest, req: NextIronRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>,
) { ) {
if (req.method === 'GET') { if (req.method === "GET") {
const client = loadStytch() const client = loadStytch();
const { token } = req.query const { token } = req.query;
try { try {
const resp = await client.magicLinks.authenticate(token as string) const resp = await client.magicLinks.authenticate(token as string);
// Set session // Set session
req.session.destroy() req.session.destroy();
req.session.set('user', { req.session.set("user", {
id: resp.user_id, id: resp.user_id,
}) });
// Save additional user data here // Save additional user data here
await req.session.save() await req.session.save();
res.redirect('/profile') res.redirect("/profile");
} catch (error) { } catch (error) {
const errorString = JSON.stringify(error) const errorString = JSON.stringify(error);
console.log(error) console.log(error);
res.status(400).json({ errorString }) res.status(400).json({ errorString });
} }
} else { } else {
// Handle any other HTTP method // 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. // This API route authenticates Stytch OTP codes.
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import { Session } from 'next-iron-session' import { Session } from "next-iron-session";
import withSession from '../../lib/withSession' import withSession from "../../lib/withSession";
import loadStytch from '../../lib/loadStytch' import loadStytch from "../../lib/loadStytch";
type NextIronRequest = NextApiRequest & { session: Session } type NextIronRequest = NextApiRequest & { session: Session };
type Data = { type Data = {
msg: string msg: string;
} };
export async function handler( export async function handler(
req: NextIronRequest, req: NextIronRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>,
) { ) {
if (req.method === 'POST') { if (req.method === "POST") {
const client = loadStytch() const client = loadStytch();
const data = JSON.parse(req.body) const data = JSON.parse(req.body);
try { try {
// params are of type stytch.LoginOrCreateUserBySMSRequest // params are of type stytch.LoginOrCreateUserBySMSRequest
const params = { const params = {
code: data.otpInput, code: data.otpInput,
method_id: data.methodId, method_id: data.methodId,
} };
const resp = await client.otps.authenticate(params) const resp = await client.otps.authenticate(params);
if (resp.status_code.toString() === '200') { if (resp.status_code.toString() === "200") {
// Set session // Set session
req.session.destroy() req.session.destroy();
// Save additional user data here // Save additional user data here
req.session.set('user', { req.session.set("user", {
id: resp.user_id, id: resp.user_id,
}) });
await req.session.save() await req.session.save();
res res
.status(200) .status(200)
.send({ msg: `successfully authenticated ${resp.user_id}` }) .send({ msg: `successfully authenticated ${resp.user_id}` });
} else { } else {
throw Error('Error authenticating your code') throw Error("Error authenticating your code");
} }
} catch (error) { } catch (error) {
const errorString = JSON.stringify(error) const errorString = JSON.stringify(error);
console.log(error) console.log(error);
res.status(400).json({ msg: errorString }) res.status(400).json({ msg: errorString });
} }
} else { } else {
// Handle any other HTTP method // 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. // This API route logs a user out.
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import { Session } from 'next-iron-session' import { Session } from "next-iron-session";
import withSession from '../../lib/withSession' import withSession from "../../lib/withSession";
type NextIronRequest = NextApiRequest & { session: Session } type NextIronRequest = NextApiRequest & { session: Session };
type Data = { type Data = {
errorString: string errorString: string;
} };
export async function handler( export async function handler(
req: NextIronRequest, req: NextIronRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>,
) { ) {
if (req.method === 'POST') { if (req.method === "POST") {
try { try {
// Set session // Set session
req.session.destroy() req.session.destroy();
res.redirect('/') res.redirect("/");
} catch (error) { } catch (error) {
const errorString = JSON.stringify(error) const errorString = JSON.stringify(error);
console.log(error) console.log(error);
res.status(400).json({ errorString }) res.status(400).json({ errorString });
} }
} else { } else {
// Handle any other HTTP method // 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. // This API route sends an OTP code to a specified number.
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import loadStytch from '../../lib/loadStytch' import loadStytch from "../../lib/loadStytch";
type Data = { type Data = {
methodId: string methodId: string;
} };
export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) { export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
if (req.method === 'POST') { if (req.method === "POST") {
const client = loadStytch() const client = loadStytch();
const data = JSON.parse(req.body) const data = JSON.parse(req.body);
try { try {
const phoneNumber = data.phoneNumber.replace(/\D/g, '') const phoneNumber = data.phoneNumber.replace(/\D/g, "");
// params are of type stytch.LoginOrCreateUserBySMSRequest // params are of type stytch.LoginOrCreateUserBySMSRequest
const params = { const params = {
phone_number: `${data.intlCode}${phoneNumber}`, phone_number: `${data.intlCode}${phoneNumber}`,
} };
const resp = await client.otps.sms.loginOrCreate(params) const resp = await client.otps.sms.loginOrCreate(params);
res.status(200).json({ methodId: resp.phone_id }) res.status(200).json({ methodId: resp.phone_id });
} catch (error) { } catch (error) {
console.log(error) console.log(error);
res.status(400) res.status(400);
} }
} else { } else {
// Handle any other HTTP method // Handle any other HTTP method
} }
} }
export default handler export default handler;

View file

@ -1,34 +1,34 @@
import React, { useEffect } from 'react' import React, { useEffect } from "react";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import { Stytch, StytchProps } from '@stytch/stytch-react' import { Stytch, StytchProps } from "@stytch/stytch-react";
import { OAuthProvidersTypes, SDKProductTypes } from '@stytch/stytch-js' import { OAuthProvidersTypes, SDKProductTypes } from "@stytch/stytch-js";
import styles from '../styles/Home.module.css' import styles from "../styles/Home.module.css";
import withSession, { ServerSideProps } from '../lib/withSession' import withSession, { ServerSideProps } from "../lib/withSession";
import LoginWithSMS from '../components/LoginWithSMS' import LoginWithSMS from "../components/LoginWithSMS";
import { LoginMethod } from '../lib/types' import { LoginMethod } from "../lib/types";
import LoginEntryPoint from '../components/LoginEntryPoint' import LoginEntryPoint from "../components/LoginEntryPoint";
// Set the URL base for redirect URLs. The three cases are as follows: // 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. // 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. // 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. // 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')) { if (process.env.NEXT_PUBLIC_VERCEL_URL?.includes("localhost")) {
REDIRECT_URL_BASE = 'http://localhost:3000' REDIRECT_URL_BASE = "http://localhost:3000";
} else if (process.env.NEXT_PUBLIC_VERCEL_URL !== undefined) { } 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 { } else {
REDIRECT_URL_BASE = 'http://localhost:3000' REDIRECT_URL_BASE = "http://localhost:3000";
} }
const stytchProps: StytchProps = { const stytchProps: StytchProps = {
loginOrSignupView: { loginOrSignupView: {
products: [SDKProductTypes.oauth, SDKProductTypes.emailMagicLinks], products: [SDKProductTypes.oauth, SDKProductTypes.emailMagicLinks],
emailMagicLinksOptions: { emailMagicLinksOptions: {
loginRedirectURL: REDIRECT_URL_BASE + '/api/authenticate_magic_link', loginRedirectURL: REDIRECT_URL_BASE + "/api/authenticate_magic_link",
loginExpirationMinutes: 30, loginExpirationMinutes: 30,
signupRedirectURL: REDIRECT_URL_BASE + '/api/authenticate_magic_link', signupRedirectURL: REDIRECT_URL_BASE + "/api/authenticate_magic_link",
signupExpirationMinutes: 30, signupExpirationMinutes: 30,
createUserAsPending: false, createUserAsPending: false,
}, },
@ -42,11 +42,11 @@ const stytchProps: StytchProps = {
}, },
style: { style: {
fontFamily: '"Helvetica New", Helvetica, sans-serif', fontFamily: '"Helvetica New", Helvetica, sans-serif',
primaryColor: '#0577CA', primaryColor: "#0577CA",
primaryTextColor: '#090909', primaryTextColor: "#090909",
width: '321px', width: "321px",
}, },
publicToken: process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN || '', publicToken: process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN || "",
callbacks: { callbacks: {
onEvent: (data: { eventData: { userId: any; email: any } }) => { onEvent: (data: { eventData: { userId: any; email: any } }) => {
if ( if (
@ -56,47 +56,49 @@ const stytchProps: StytchProps = {
console.log({ console.log({
userId: data.eventData.userId, userId: data.eventData.userId,
email: data.eventData.email, email: data.eventData.email,
}) });
} else { } else {
console.warn('The user is not found. Data: ', data) console.warn("The user is not found. Data: ", data);
} }
}, },
onSuccess: (data) => console.log(data), onSuccess: (data) => console.log(data),
onError: (data) => console.log(data), onError: (data) => console.log(data),
}, },
} };
type Props = { type Props = {
publicToken: string publicToken: string;
user: { user: {
id: string id: string;
} };
} };
const App = (props: Props) => { const App = (props: Props) => {
const { user, publicToken } = props const { user, publicToken } = props;
const [loginMethod, setLoginMethod] = React.useState<LoginMethod | null>(null) const [loginMethod, setLoginMethod] = React.useState<LoginMethod | null>(
const router = useRouter() null,
);
const router = useRouter();
useEffect(() => { useEffect(() => {
if (user) { if (user) {
router.push('/profile') router.push("/profile");
} }
}) });
const loginMethodMap: Record<LoginMethod, React.ReactElement> = { const loginMethodMap: Record<LoginMethod, React.ReactElement> = {
[LoginMethod.API]: <LoginWithSMS />, [LoginMethod.API]: <LoginWithSMS />,
[LoginMethod.SDK]: ( [LoginMethod.SDK]: (
<div className={styles.container}> <div className={styles.container}>
<Stytch <Stytch
publicToken={publicToken || ''} publicToken={publicToken || ""}
loginOrSignupView={stytchProps.loginOrSignupView} loginOrSignupView={stytchProps.loginOrSignupView}
style={stytchProps.style} style={stytchProps.style}
callbacks={stytchProps.callbacks} callbacks={stytchProps.callbacks}
/> />
</div> </div>
), ),
} };
return ( return (
<div className={styles.root}> <div className={styles.root}>
@ -106,19 +108,19 @@ const App = (props: Props) => {
loginMethodMap[loginMethod] loginMethodMap[loginMethod]
)} )}
</div> </div>
) );
} };
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => { const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
// Get the user's session based on the request // 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 = { const props: Props = {
publicToken: stytchProps.publicToken, publicToken: stytchProps.publicToken,
user, 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 React, { useEffect } from "react";
import styles from '../styles/Home.module.css' import styles from "../styles/Home.module.css";
import StytchContainer from '../components/StytchContainer' import StytchContainer from "../components/StytchContainer";
import withSession, { ServerSideProps } from '../lib/withSession' import withSession, { ServerSideProps } from "../lib/withSession";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
type Props = { type Props = {
user?: { user?: {
id: string id: string;
} };
} };
const Profile = (props: Props) => { const Profile = (props: Props) => {
const { user } = props const { user } = props;
const router = useRouter() const router = useRouter();
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
router.replace('/') router.replace("/");
} }
}) });
const signOut = async () => { const signOut = async () => {
const resp = await fetch('/api/logout', { method: 'POST' }) const resp = await fetch("/api/logout", { method: "POST" });
if (resp.status === 200) { if (resp.status === 200) {
router.push('/') router.push("/");
}
} }
};
return ( return (
<> <>
@ -33,12 +33,12 @@ const Profile = (props: Props) => {
<div /> <div />
) : ( ) : (
<StytchContainer> <StytchContainer>
<h2>{'Welcome!'}</h2> <h2>{"Welcome!"}</h2>
<p className={styles.profileSubHeader}> <p className={styles.profileSubHeader}>
Thank you for using Stytch! Heres your user info. Thank you for using Stytch! Heres your user info.
</p> </p>
<pre className={styles.code}> <pre className={styles.code}>
{JSON.stringify(user, null, 1).replace(' ', '')} {JSON.stringify(user, null, 1).replace(" ", "")}
</pre> </pre>
<button className={styles.primaryButton} onClick={signOut}> <button className={styles.primaryButton} onClick={signOut}>
Sign out Sign out
@ -46,16 +46,16 @@ const Profile = (props: Props) => {
</StytchContainer> </StytchContainer>
)} )}
</> </>
) );
} };
const getServerSidePropsHandler: ServerSideProps = async ({ req }) => { const getServerSidePropsHandler: ServerSideProps = async ({ req }) => {
// Get the user's session based on the request // 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 = { user } const props: Props = { user };
return { props } 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