Source E:\Desktop\Youtube 2020 Nextjs\Next.js Building a Car Trader App
C:\Users\Administrator\Desktop\nextjs\pages\cars.tsx
Copy import { Grid } from '@material-ui/core';
import deepEqual from 'fast-deep-equal';
import { GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
import { stringify } from 'querystring';
import { useState } from 'react';
import useSWR from 'swr';
import Search from '.';
import { CarModel } from '../../api/Car';
import { CarCard } from '../components/CarCard';
import { CarPagination } from '../components/CarPagination';
import { getMakes, Make } from './database/getMakes';
import { getModels, Model } from './database/getModels';
import { getPaginatedCars } from './database/getPaginatedCars';
import { getAsString } from '../getAsString';
export interface CarsListProps {
makes: Make[];
models: Model[];
cars: CarModel[];
totalPages: number;
}
export default function CarsList({
makes,
models,
cars,
totalPages,
}: CarsListProps) {
const { query } = useRouter();
const [serverQuery] = useState(query);
const { data } = useSWR('/api/cars?' + stringify(query), {
dedupingInterval: 15000,
initialData: deepEqual(query, serverQuery)
? { cars, totalPages }
: undefined,
});
return (
<Grid container spacing={3}>
<Grid item xs={12} sm={5} md={3} lg={2}>
<Search singleColumn makes={makes} models={models} />
</Grid>
<Grid container item xs={12} sm={7} md={9} lg={10} spacing={3}>
<Grid item xs={12}>
<CarPagination totalPages={data?.totalPages} />
</Grid>
{(data?.cars || []).map((car) => (
<Grid key={car.id} item xs={12} sm={6}>
<CarCard car={car} />
</Grid>
))}
<Grid item xs={12}>
<CarPagination totalPages={data?.totalPages} />
</Grid>
</Grid>
</Grid>
);
}
export const getServerSideProps: GetServerSideProps<CarsListProps> = async (
ctx
) => {
const make = getAsString(ctx.query.make);
const [makes, models, pagination] = await Promise.all([
getMakes(),
getModels(make),
getPaginatedCars(ctx.query),
]);
return {
props: {
makes,
models,
cars: pagination.cars,
totalPages: pagination.totalPages,
},
};
};
C:\Users\Administrator\Desktop\nextjs\pages\database\getPaginatedCars.ts
Copy import { ParsedUrlQuery } from 'querystring';
import { CarModel } from '../../api/Car';
import { getAsString } from '../../getAsString';
import openDB from '../../openDB';
const mainQuery = `
FROM car
WHERE (@make is NULL OR @make = make)
AND (@model is NULL OR @model = model)
AND (@minPrice is NULL OR @minPrice <= price)
AND (@maxPrice is NULL OR @maxPrice >= price)
`;
export async function getPaginatedCars(query: ParsedUrlQuery) {
const db = await openDB();
const page = getValueNumber(query.page) || 1;
const rowsPerPage = getValueNumber(query.rowsPerPage) || 4;
const offset = (page - 1) * rowsPerPage;
const dbParams = {
'@make': getValueStr(query.make),
'@model': getValueStr(query.model),
'@minPrice': getValueNumber(query.minPrice),
'@maxPrice': getValueNumber(query.maxPrice),
};
const carsPromise = db.all<CarModel[]>(
`SELECT * ${mainQuery} LIMIT @rowsPerPage OFFSET @offset`,
{
...dbParams,
'@rowsPerPage': rowsPerPage,
'@offset': offset,
}
);
const totalRowsPromise = db.get<{ count: number }>(
`SELECT COUNT(*) as count ${mainQuery}`,
dbParams
);
const [cars, totalRows] = await Promise.all([carsPromise, totalRowsPromise]);
return { cars, totalPages: Math.ceil(totalRows.count / rowsPerPage) };
}
function getValueNumber(value: string | string[]) {
const str = getValueStr(value);
const number = parseInt(str);
return isNaN(number) ? null : number;
}
function getValueStr(value: string | string[]) {
const str = getAsString(value);
return !str || str.toLowerCase() === 'all' ? null : str;
}
C:\Users\Administrator\Documents\car\app-6\src\database\getMakes.ts
Copy 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\Documents\car\app-6\src\database\getModels.ts
Copy 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\Documents\car\app-6\src\pages\api\cars.ts
Copy import { NextApiRequest, NextApiResponse } from 'next';
import { getPaginatedCars } from '../../database/getPaginatedCars';
export default async function cars(
req: NextApiRequest,
res: NextApiResponse
) {
const cars = await getPaginatedCars(req.query);
res.json(cars);
}
C:\Users\Administrator\Desktop\nextjs\pages\api\cars.ts
Copy import { NextApiRequest, NextApiResponse } from 'next';
import { getPaginatedCars } from '../database/getPaginatedCars';
export default async function cars(
req: NextApiRequest,
res: NextApiResponse
) {
const cars = await getPaginatedCars(req.query);
res.json(cars);
}
C:\Users\Administrator\Desktop\nextjs\components\CarCard.tsx
Copy import Avatar from '@material-ui/core/Avatar';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
import CardMedia from '@material-ui/core/CardMedia';
import { red } from '@material-ui/core/colors';
import IconButton from '@material-ui/core/IconButton';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import Link from 'next/link';
import React from 'react';
import { CarModel } from '../../api/Car';
export interface CarCardProps {
car: CarModel;
}
const useStyles = makeStyles((theme) => ({
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
expand: {
transform: 'rotate(0deg)',
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
},
expandOpen: {
transform: 'rotate(180deg)',
},
avatar: {
backgroundColor: red[500],
},
achorTag: {
textDecoration: 'none'
}
}));
export function CarCard({ car }: CarCardProps) {
const classes = useStyles();
return (
<Link
href="/car/[make]/[brand]/[id]"
as={`/car/${car.make}/${car.model}/${car.id}`}
>
<a className={classes.achorTag}>
<Card>
<CardHeader
avatar={
<Avatar aria-label="recipe" className={classes.avatar}>
R
</Avatar>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title={car.make + ' ' + car.model}
subheader={`£${car.price}`}
/>
<CardMedia
className={classes.media}
image={car.photoUrl}
title={car.make + ' ' + car.model}
/>
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">
{car.details}
</Typography>
</CardContent>
</Card>
</a>
</Link>
);
}
C:\Users\Administrator\Desktop\nextjs\components\CarPagination.tsx
Copy import { PaginationRenderItemParams } from '@material-ui/lab';
import Pagination from '@material-ui/lab/Pagination';
import PaginationItem from '@material-ui/lab/PaginationItem';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ParsedUrlQuery } from 'querystring';
import { forwardRef } from 'react';
import { getAsString } from '../getAsString';
export function CarPagination({ totalPages }: { totalPages: number }) {
const { query } = useRouter();
return (
<Pagination
page={parseInt(getAsString(query.page) || '1')}
count={totalPages}
renderItem={(item) => (
<PaginationItem
component={MaterialUiLink}
query={query}
item={item}
{...item}
/>
)}
/>
);
}
export interface MaterialUiLinkProps {
item: PaginationRenderItemParams;
query: ParsedUrlQuery;
}
const MaterialUiLink = forwardRef<HTMLAnchorElement, MaterialUiLinkProps>(
({ item, query, ...props }, ref) => (
<Link
href={{
pathname: '/cars',
query: { ...query, page: item.page },
}}
shallow
>
<a {...props} ref={ref}></a>
</Link>
)
);
C:\Users\Administrator\Desktop\nextjs\components\cars.tsx
Copy import { Grid } from '@material-ui/core';
import { PaginationRenderItemParams } from '@material-ui/lab';
import Pagination from '@material-ui/lab/Pagination';
import PaginationItem from '@material-ui/lab/PaginationItem';
import { GetServerSideProps } from 'next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ParsedUrlQuery } from 'querystring';
import Search from '.';
import { CarModel } from '../../api/Car';
import { getMakes, Make } from './database/getMakes';
import { getModels, Model } from './database/getModels';
import { getPaginatedCars } from './database/getPaginatedCars';
import { getAsString } from '../getAsString';
export interface CarsListProps {
makes: Make[];
models: Model[];
cars: CarModel[];
totalPages: number;
}
export default function CarsList({
makes,
models,
cars,
totalPages,
}: CarsListProps) {
const { query } = useRouter();
return (
<Grid container spacing={3}>
<Grid item xs={12} sm={5} md={3} lg={2}>
<Search singleColumn makes={makes} models={models} />
</Grid>
<Grid container item xs={12} sm={7} md={9} lg={10} spacing={3}>
<Grid item xs={12}>
<CarPagination totalPages={data?.totalPages} />
</Grid>
{(data?.cars || []).map((car) => (
<Grid key={car.id} item xs={12} sm={6}>
<CarCard car={car} />
</Grid>
))}
<Grid item xs={12}>
<CarPagination totalPages={data?.totalPages} />
</Grid>
</Grid>
</Grid>
);
}
export interface MaterialUiLinkProps {
item: PaginationRenderItemParams;
query: ParsedUrlQuery;
}
export function MaterialUiLink({ item, query, ...props }: MaterialUiLinkProps) {
return (
<Link
href={{
pathname: '/cars',
query: { ...query, page: item.page },
}}
>
<a {...props}></a>
</Link>
);
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const make = getAsString(ctx.query.make);
const [makes, models, pagination] = await Promise.all([
getMakes(),
getModels(make),
getPaginatedCars(ctx.query),
]);
return {
props: {
makes,
models,
cars: pagination.cars,
totalPages: pagination.totalPages,
},
};
};