Skip to content

Commit

Permalink
validate monthName route param, add 404 and 500 pages
Browse files Browse the repository at this point in the history
  • Loading branch information
nemanjam committed Dec 8, 2024
1 parent bb1807a commit bcdf32c
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-push-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Build and push Docker
on:
push:
branches:
- 'disabled-main'
- 'main'
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
pull_request:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Deploy Docker
on:
workflow_run:
workflows:
- 'disabled-Build and push Docker'
- 'Build and push Docker'
types:
- completed

Expand Down
4 changes: 4 additions & 0 deletions app/[[...month]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FC } from 'react';
import { notFound } from 'next/navigation';

import LineChartMultiple from '@/components/charts/line-chart-multiple';
import Heading from '@/components/heading';
Expand All @@ -8,6 +9,7 @@ import { getNewOldCompaniesForMonthCached } from '@/modules/database/select/comp
import { getNewOldCompaniesCountForAllMonthsCached } from '@/modules/database/select/line-chart';
import { getAllMonths } from '@/modules/database/select/month';
import { getStatisticsCached } from '@/modules/database/select/statistics';
import { isValidMonthNameWithDb } from '@/utils/validation';
import { METADATA } from '@/constants/metadata';

import { MonthQueryParam } from '@/types/website';
Expand All @@ -26,6 +28,8 @@ const IndexPage: FC<Props> = async ({ params }) => {
const { month } = await params;
const selectedMonth = month?.[0] ?? allMonths[0].name;

if (!isValidMonthNameWithDb(selectedMonth)) return notFound();

const newOldCompanies = await getNewOldCompaniesForMonthCached(selectedMonth);

const { monthsCount, companiesCount, commentsCount, firstMonth, lastMonth } = statistics ?? {
Expand Down
57 changes: 57 additions & 0 deletions app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { FC } from 'react';
import Link from 'next/link';

import { RefreshCcw, Settings } from 'lucide-react';

import { Button } from '@/components/ui/button';

import { fontSans } from '@/libs/fonts';
import { cn } from '@/utils/styles';

import '@/styles/globals.css';

interface Props {
error: Error & { digest?: string };
reset: () => void;
}

const GlobalError: FC<Props> = ({ error, reset }) => (
<html>
<body
className={cn(
'relative min-h-screen min-w-80 flex flex-col bg-background font-sans antialiased',
fontSans.variable
)}
>
<main className="flex-1 flex flex-col justify-center items-center p-4">
<div className="w-full max-w-md space-y-4 text-center">
<div className="flex justify-center">
<Settings className="size-24 text-muted-foreground" />
</div>

<h1 className="text-3xl font-extrabold tracking-tight">500 - Server Error</h1>
<p className="text-muted-foreground">
We are sorry, but something went wrong on our end. Please try again later.
</p>

<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button onClick={() => reset()} variant="outline">
<RefreshCcw className="mr-2 size-4" /> Try Again
</Button>
<Button asChild>
<Link href="/">Return to Home</Link>
</Button>
</div>

{error.digest && (
<p className="text-sm text-muted-foreground">Error ID: {error.digest}</p>
)}
</div>
</main>
</body>
</html>
);

export default GlobalError;
42 changes: 22 additions & 20 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { SERVER_CONFIG } from '@/config/server';

import '@/styles/globals.css';

import { FC } from 'react';

// single in layout is enough for all pages
export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -69,24 +71,24 @@ interface RootLayoutProps {
children: React.ReactNode;
}

export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<BaseHead />
<body
className={cn(
'relative min-h-screen min-w-80 flex flex-col bg-background font-sans antialiased',
fontSans.variable
)}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<Header />
<main className="flex-1 my-container py-8 md:py-10">{children}</main>
<Footer />
const RootLayout: FC<RootLayoutProps> = async ({ children }) => (
<html lang="en" suppressHydrationWarning>
<BaseHead />
<body
className={cn(
'relative min-h-screen min-w-80 flex flex-col bg-background font-sans antialiased',
fontSans.variable
)}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<Header />
<main className="flex-1 flex flex-col my-container py-8 md:py-10">{children}</main>
<Footer />

<TailwindIndicator />
</ThemeProvider>
</body>
</html>
);
}
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
);

export default RootLayout;
4 changes: 4 additions & 0 deletions app/month/[[...month]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FC } from 'react';
import { notFound } from 'next/navigation';

import BarChartSimple from '@/components/charts/bar-chart-simple';
import CompaniesCommentsTable from '@/components/companies-comments-table';
Expand All @@ -8,6 +9,7 @@ import { getNewOldCompaniesForMonthCached } from '@/modules/database/select/comp
import { getAllMonths } from '@/modules/database/select/month';
import { getBarChartSimpleData } from '@/modules/transform/bar-chart';
import { getCompanyTableData } from '@/modules/transform/table';
import { isValidMonthNameWithDb } from '@/utils/validation';

import { MonthQueryParam } from '@/types/api';

Expand All @@ -19,6 +21,8 @@ const CurrentMonthPage: FC<Props> = async ({ params }) => {
const { month } = await params;
const selectedMonth = month?.[0] ?? allMonths[0].name;

if (!isValidMonthNameWithDb(selectedMonth)) return notFound();

const newOldCompanies = await getNewOldCompaniesForMonthCached(selectedMonth);
const companyTableData = getCompanyTableData(newOldCompanies.allCompanies);
const barChartSimpleData = getBarChartSimpleData(newOldCompanies.allCompanies);
Expand Down
27 changes: 27 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FC } from 'react';
import Link from 'next/link';

import { Cat } from 'lucide-react';

import { Button } from '@/components/ui/button';

const NotFound: FC = () => (
<div className="flex-1 flex flex-col justify-center items-center">
<div className="space-y-4 text-center">
<div className="flex justify-center">
<Cat className="size-24 text-muted-foreground" />
</div>

<h1 className="text-3xl font-extrabold tracking-tight">404 - Page Not Found</h1>
<p className="text-muted-foreground">Oops! The page you are looking for does not exist.</p>

<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button asChild variant="outline">
<Link href="/">Return to Home</Link>
</Button>
</div>
</div>
</div>
);

export default NotFound;
3 changes: 3 additions & 0 deletions constants/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const VALIDATION = {
monthNameRegex: /^\d{4}-\d{2}$/,
} as const;
5 changes: 3 additions & 2 deletions docs/working-notes/notes4.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
record video demo
database diagram
fix bar chart 8-12
fix first month exception
add 404 and 500 page
fix first month exception
add 404 and 500 page
validate month in page params
6 changes: 1 addition & 5 deletions libs/datetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ const { appTimeZone, appDateTimeFormat } = SERVER_CONFIG;
export const DATETIME = {
monthNameFormat: 'yyyy-MM',
sanFranciscoTimeZone: 'America/Los_Angeles',
monthNameRegex: /^\d{4}-\d{2}$/,
} as const;

const { monthNameFormat, sanFranciscoTimeZone, monthNameRegex } = DATETIME;
const { monthNameFormat, sanFranciscoTimeZone } = DATETIME;

/**
* Format to 'YYYY-MM'.
Expand Down Expand Up @@ -88,6 +87,3 @@ export const createOldMonthName = (monthName: string, monthsAgo: number): string
const previousDate = subMonths(date, monthsAgo);
return format(previousDate, monthNameFormat);
};

/** Used only in db to validate for delete. */
export const isValidMonthName = (monthName: string): boolean => monthNameRegex.test(monthName);
2 changes: 1 addition & 1 deletion modules/database/delete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getDb } from '@/modules/database/schema';
import { isValidMonthName } from '@/libs/datetime';
import { isValidMonthName } from '@/utils/validation';
import { ALGOLIA } from '@/constants/algolia';

const { threads } = ALGOLIA;
Expand Down
1 change: 1 addition & 0 deletions modules/database/select/month.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getDb } from '@/modules/database/schema';

import { DbMonth, MonthPair } from '@/types/database';

// never cache this, validate month slug
export const getMonthByName = (monthName: string): DbMonth => {
const month = getDb()
.prepare<string, DbMonth>(`SELECT * FROM month WHERE name = ?`)
Expand Down
11 changes: 11 additions & 0 deletions utils/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getMonthByName } from '@/modules/database/select/month';
import { VALIDATION } from '@/constants/validation';

const { monthNameRegex } = VALIDATION;

/** Used only in db to validate for delete. */
export const isValidMonthName = (monthName: string): boolean => monthNameRegex.test(monthName);

/** In pages, validate month slug. */
export const isValidMonthNameWithDb = (monthName: string): boolean =>
isValidMonthName(monthName) && Boolean(getMonthByName(monthName));

0 comments on commit bcdf32c

Please sign in to comment.