Next.js Building a Car Trader App #4- Model - Dynamic Dropdown using Formik and SWR
Last updated
Was this helpful?
Last updated
Was this helpful?
Xây dựng api/models
[
{
"model": "A2",
"count": 1
}
]
import { NextApiRequest, NextApiResponse } from 'next';
import { getModels } from '../database/getModels';
import { getAsString } from '../../getAsString';
export default async function models(req: NextApiRequest,res: NextApiResponse) {
const make = getAsString(req.query.make);
const models = await getModels(make);
res.json(models);
}
C:\Users\Administrator\Desktop\pro\pages\database\getModels.tsx
import openDB from "../../openDB";
export interface Model {
model: string;
count: number;
}
export async function getModels(make: string) {
const db = await openDB();
const model = await db.all < Model[] > (`SELECT model, count(*) as count FROM car WHERE make = @make GROUP BY model`, { '@make': make });
return model;
}
C:\Users\Administrator\Desktop\nextjs\pages\index.tsx
import { Button, FormControl, Grid, InputLabel, makeStyles, MenuItem, Paper, Select, SelectProps } from '@material-ui/core';
import { Field, Form, Formik, useField, useFormikContext } from 'formik';
import { GetServerSideProps } from 'next';
import router, { useRouter } from 'next/router';
import useSWR from 'swr';
import { getMakes, Make } from './database/getMakes';
import { getModels, Model } from './database/getModels';
import { getAsString } from '../getAsString';
export interface HomeProps {
makes: Make[];
models: Model[];
}
const useStyles = makeStyles((theme) => ({
paper: {
margin: 'auto',
maxWidth: 500,
padding: theme.spacing(3),
},
}));
const prices = [500, 1000, 5000, 15000, 25000, 50000, 250000];
export default function Home({ makes, models }: HomeProps) {
const classes = useStyles();
const { query } = useRouter();
const initialValues = {
make: getAsString(query.make) || 'all',
model: getAsString(query.model) || 'all',
minPrice: getAsString(query.minPrice) || 'all',
maxPrice: getAsString(query.maxPrice) || 'all',
};
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => {
router.push(
{
pathname: '/',
query: { ...values, page: 1 },
},
undefined,
{ shallow: true }
);
}}
>
{({ values }) => (
<Form>
<Paper elevation={5} className={classes.paper}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth variant="outlined">
<InputLabel id="search-make">Make</InputLabel>
<Field
name="make"
as={Select}
labelId="search-make"
label="Make"
>
<MenuItem value="all">
<em>All Makes</em>
</MenuItem>
{makes.map((make) => (
<MenuItem key={make.make} value={make.make}>
{`${make.make} (${make.count})`}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<ModelSelect make={values.make} name="model" models={models} />
</Grid>
<Grid item xs={12} sm={6}>
<FormControl fullWidth variant="outlined">
<InputLabel id="search-min-price">Min Price</InputLabel>
<Field
name="minPrice"
as={Select}
labelId="search-min-price"
label="Min Price"
>
<MenuItem value="all">
<em>No Min</em>
</MenuItem>
{prices.map((price) => (
<MenuItem key={price} value={price}>
{price}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<FormControl fullWidth variant="outlined">
<InputLabel id="search-max-price">Max Price</InputLabel>
<Field
name="maxPrice"
as={Select}
labelId="search-max-price"
label="Max Price"
>
<MenuItem value="all">
<em>No Max</em>
</MenuItem>
{prices.map((price) => (
<MenuItem key={price} value={price}>
{price}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
>
Search
</Button>
</Grid>
</Grid>
</Paper>
</Form>
)}
</Formik>
);
}
export interface ModelSelectProps extends SelectProps {
name: string;
models: Model[];
make: string;
}
export function ModelSelect({ models, make, ...props }: ModelSelectProps) {
const { setFieldValue } = useFormikContext();
const [field] = useField({
name: props.name,
});
const { data } = useSWR<Model[]>('/api/models?make=' + make, {
onSuccess: (newValues) => {
if (!newValues.map((a) => a.model).includes(field.value)) {
setFieldValue('model', 'all');
}
},
});
const newModels = data || models;
return (
<FormControl fullWidth variant="outlined">
<InputLabel id="search-model">Model</InputLabel>
<Select
name="model"
labelId="search-model"
label="Model"
{...field}
{...props}
>
<MenuItem value="all">
<em>All Models</em>
</MenuItem>
{newModels.map((model) => (
<MenuItem key={model.model} value={model.model}>
{`${model.model} (${model.count})`}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const make = getAsString(ctx.query.make);
const [makes, models] = await Promise.all([getMakes(), getModels(make)]);
return { props: { makes, models } };
};
C:\Users\Administrator\Desktop\pro\getAsString.tsx
export function getAsString(value: string|string[]): string {
if(Array.isArray(value)) {
return value[0];
}
return value;
}
C:\Users\Administrator\Desktop\nextjs\api\Car.ts
export interface CarModel {
id: number;
make: string;
model: string;
year: number;
fuelType: string;
kilometers: number;
details: string;
price: number;
photoUrl: string;
}
C:\Users\Administrator\Desktop\nextjs\api\Faq.ts
export interface FaqModel {
id: number;
question: string;
answer: string;
}
C:\Users\Administrator\Desktop\nextjs\components\Nav.tsx
import { AppBar, Button, makeStyles, Toolbar, Typography } from '@material-ui/core';
import Link from 'next/link';
import React from 'react';
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1
},
menuButton: {
marginRight: theme.spacing(2)
},
title: {
flexGrow: 1
}
}));
export function Nav() {
const classes = useStyles();
return (
<AppBar position="static">
<Toolbar variant="dense">
<Typography variant="h6" className={classes.title}>
Car Trader
</Typography>
<Button color="inherit">
<Link href="/">
<a style={{ color: 'white' }}>
<Typography color="inherit">
Home
</Typography>
</a>
</Link>
</Button>
<Button color="inherit">
<Link href="/faq">
<a style={{ color: 'white' }}>
<Typography color="inherit">
FAQ
</Typography>
</a>
</Link>
</Button>
</Toolbar>
</AppBar>
);
}
C:\Users\Administrator\Desktop\nextjs\pages\api\models.ts
import { NextApiRequest, NextApiResponse } from "next";
import { getModels } from "../database/getModels";
import { getAsString } from "../../getAsString";
export default async function models(req: NextApiRequest, res: NextApiResponse){
const make = getAsString(req.query.make);
const models = await getModels(make);
res.json(models);
}
C:\Users\Administrator\Desktop\nextjs\pages\car[make][brand][id].tsx
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import { GetServerSideProps } from 'next';
import React from 'react';
import CarModel from '../../../../api/Car';
import openDB from '../../../../openDB';
const useStyles = makeStyles((theme) => ({
paper: {
padding: theme.spacing(2),
margin: 'auto',
},
img: {
width: '100%',
},
}));
interface CarDetailsProps {
car: CarModel | null | undefined;
}
export default function CarDetails({ car }: CarDetailsProps) {
const classes = useStyles();
if (!car) {
return <h1>Sorry, car not found!</h1>;
}
return (
<div>
<Paper className={classes.paper}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={5}>
<img className={classes.img} alt="complex" src={car.photoUrl} />
</Grid>
<Grid item xs={12} sm={6} md={7} container>
<Grid item xs container direction="column" spacing={2}>
<Grid item xs>
<Typography variant="h5">
{car.make + ' ' + car.model}
</Typography>
<Typography gutterBottom variant="h4">
£{car.price}
</Typography>
<Typography gutterBottom variant="body2" color="textSecondary">
Year: {car.year}
</Typography>
<Typography gutterBottom variant="body2" color="textSecondary">
KMs: {car.kilometers}
</Typography>
<Typography gutterBottom variant="body2" color="textSecondary">
Fuel Type: {car.fuelType}
</Typography>
<Typography gutterBottom variant="body1" color="textSecondary">
Details: {car.details}
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</Paper>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const id = ctx.params.id;
const db = await openDB();
const car = await db.get<CarModel | undefined>(
'SELECT * FROM Car where id = ?',
id
);
return { props: { car: car || null } };
};
C:\Users\Administrator\Desktop\nextjs\pages\database\getMakes.ts
import openDB from "../../openDB";
export interface Make {
make: string;
count: number;
}
export async function getMakes() {
const db = await openDB();
const makes = await db.all<Make[]>(`SELECT make, count(*) as count FROM car GROUP BY make`);
return makes;
}
C:\Users\Administrator\Desktop\nextjs\pages\database\getModels.tsx
import openDB from "../../openDB";
export interface Model {
model: string;
count: number;
}
export async function getModels(make: string) {
const db = await openDB();
const model = await db.all<Model[]>(`SELECT model, count(*) as count FROM car WHERE make = @make GROUP BY model`, {'@make': make});
return model;
}
C:\Users\Administrator\Desktop\nextjs\pages\_app.tsx
import { Box, Container, CssBaseline } from '@material-ui/core';
import red from '@material-ui/core/colors/red';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import axios from 'axios';
import App from 'next/app';
import Head from 'next/head';
import React from 'react';
import { SWRConfig } from 'swr';
import { Nav } from '../components/Nav';
// Create a theme instance.
export const theme = createMuiTheme({
palette: {
primary: {
main: '#556cd6'
},
error: {
main: red.A400
},
background: {
default: '#fff'
}
}
});
export default class MyApp extends App {
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement!.removeChild(jssStyles);
}
}
render() {
const { Component, pageProps } = this.props;
return (
<React.Fragment>
<Head>
<title>My page</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Nav />
<SWRConfig
value={{ fetcher: (url: string) => axios(url).then(r => r.data) }}
>
<Container maxWidth={false}>
<Box marginTop={2}>
<Component {...pageProps} />
</Box>
</Container>
</SWRConfig>
</ThemeProvider>
</React.Fragment>
);
}
}
C:\Users\Administrator\Desktop\nextjs\pages\_document.tsx
import { ServerStyleSheets } from '@material-ui/core/styles';
import Document, { Html, Head, Main, NextScript } from 'next/document'
import React from 'react';
import { theme } from './_app';
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async ctx => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};
};
C:\Users\Administrator\Desktop\nextjs\pages\faq.tsx
import { ExpansionPanel, ExpansionPanelDetails, Typography } from '@material-ui/core';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import Accordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionActions'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { GetStaticProps } from 'next';
import { FaqModel } from '../api/Faq';
import openDB from '../openDB';
interface FaqProps {
faq: FaqModel[];
}
export default function Faq({ faq }: FaqProps) {
return (
<div>
{faq.map((f) => (
<Accordion key={f.id}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography>
{f.question}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
{f.answer}
</Typography>
</AccordionDetails>
</Accordion>
))}
</div>
);
}
export const getStaticProps: GetStaticProps = async () => {
const db = await openDB();
const faq = await db.all('SELECT * FROM FAQ ORDER BY createDate DESC');
return {
props: { faq }
};
};