"}: ${error.message}`
29 | );
30 | }
31 | : undefined,
32 | });
33 |
34 | export { handler as GET, handler as POST };
35 |
--------------------------------------------------------------------------------
/src/app/history/layout.tsx:
--------------------------------------------------------------------------------
1 | import "~/styles/globals.css";
2 |
3 | import { type Metadata } from "next";
4 |
5 | export const metadata: Metadata = {
6 | title: "Latest JS framework releases",
7 | description: "History of JavaScript frameworks and their release dates.",
8 | keywords: "javaScript, framework, counter, last, js, days, since, release",
9 | icons: [{ rel: "icon", url: "/favicon.svg" }],
10 | };
11 |
12 | export default function Layout({
13 | children,
14 | }: Readonly<{ children: React.ReactNode }>) {
15 | return children;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/history/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ErrorPage from "~/components/Error";
4 | import HistoryPage from "~/components/History";
5 | import Loading from "~/components/Loading";
6 |
7 | import { api } from "~/trpc/react";
8 |
9 | export default function Home() {
10 | const getCounterQuery = api.framework.getHistory.useQuery();
11 |
12 | return (
13 |
14 | {getCounterQuery.isLoading && }
15 | {getCounterQuery.isError && }
16 | {getCounterQuery.isSuccess && (
17 |
18 | )}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "~/styles/globals.css";
2 |
3 | import { GeistSans } from "geist/font/sans";
4 | import { type Metadata } from "next";
5 |
6 | import Footer from "~/components/Footer";
7 | import Navbar from "~/components/Navbar";
8 | import { TRPCReactProvider } from "~/trpc/react";
9 |
10 | export const metadata: Metadata = {
11 | title: "Days since last JS framework",
12 | description:
13 | "Counter for the days since the release of the latest JavaScript framework",
14 | keywords: "javaScript, framework, counter, last, js, days, since, release",
15 | icons: [{ rel: "icon", url: "/favicon.svg" }],
16 | };
17 |
18 | export default function RootLayout({
19 | children,
20 | }: Readonly<{ children: React.ReactNode }>) {
21 | return (
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Counter from "~/components/Counter";
4 | import ErrorPage from "~/components/Error";
5 | import Loading from "~/components/Loading";
6 |
7 | import { api } from "~/trpc/react";
8 |
9 | export default function Home() {
10 | const getCounterQuery = api.framework.getCounter.useQuery();
11 |
12 | return (
13 |
14 | {getCounterQuery.isLoading && }
15 | {getCounterQuery.isError && }
16 | {getCounterQuery.isSuccess && }
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/runtimes/layout.tsx:
--------------------------------------------------------------------------------
1 | import "~/styles/globals.css";
2 |
3 | import { type Metadata } from "next";
4 |
5 | export const metadata: Metadata = {
6 | title: "JS runtimes",
7 | description: "History of JavaScript runtimes and their release dates.",
8 | keywords: "javaScript, runtime, counter, last, js, days, since, release",
9 | icons: [{ rel: "icon", url: "/favicon.svg" }],
10 | };
11 |
12 | export default function Layout({
13 | children,
14 | }: Readonly<{ children: React.ReactNode }>) {
15 | return children;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/runtimes/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ErrorPage from "~/components/Error";
4 | import Loading from "~/components/Loading";
5 | import RuntimesPage from "~/components/Runtimes";
6 |
7 | import { api } from "~/trpc/react";
8 |
9 | export default function Home() {
10 | const query = api.framework.getRuntimes.useQuery();
11 |
12 | return (
13 |
14 | {query.isLoading && }
15 | {query.isError && }
16 | {query.isSuccess && }
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Counter.tsx:
--------------------------------------------------------------------------------
1 | import { type EntityData } from "~/server/types";
2 |
3 | type Props = {
4 | count: number;
5 | date: string;
6 | framework: EntityData;
7 | };
8 |
9 | export default function Counter(props: Props) {
10 | const linkHref = props.framework.link ?? props.framework.releaseLink ?? "";
11 |
12 | return (
13 |
14 |
It's been
15 |
{props.count}
16 |
day(s) since the latest JavaScript framework.
17 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Error.tsx:
--------------------------------------------------------------------------------
1 | export default function ErrorPage() {
2 | return (
3 |
4 |
An error occurred
5 |
Please try reloading the page
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | export default function Footer() {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/History.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { type HistoryData } from "~/server/types";
4 | import ListPage from "./List";
5 |
6 | type Props = {
7 | frameworks: HistoryData[];
8 | };
9 |
10 | export default function HistoryPage(props: Props) {
11 | return (
12 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/List.tsx:
--------------------------------------------------------------------------------
1 | import { type HistoryData, type RuntimeData } from "~/server/types";
2 | import { formatDate } from "~/util/date";
3 |
4 | type Props = {
5 | data: HistoryData[] | RuntimeData[];
6 | title: string;
7 | firstCol: string;
8 | };
9 |
10 | export default function ListPage(props: Props) {
11 | console.log(props.data);
12 | return (
13 | <>
14 | {props.title}
15 |
16 |
{props.firstCol}
17 |
Release
18 |
Days
19 | {props.data.map((entity) => (
20 |
21 | {entity.entity.link ? (
22 |
28 | {entity.entity.name}
29 |
30 | ) : (
31 |
{entity.entity.name}
32 | )}
33 | {entity.entity.releaseLink ? (
34 |
44 | ) : (
45 |
{formatDate(entity.entity.date)}
46 | )}
47 |
{entity.count ?? "-"}
48 |
49 | ))}
50 |
51 | >
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return (
3 |
4 | Loading...
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function Navbar() {
4 | return (
5 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Runtimes.tsx:
--------------------------------------------------------------------------------
1 | import { type RuntimeData, type HistoryData } from "~/server/types";
2 | import ListPage from "./List";
3 |
4 | type Props = {
5 | runtimes: RuntimeData[];
6 | };
7 |
8 | export default function RuntimesPage(props: Props) {
9 | return (
10 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/env.js:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | /**
6 | * Specify your server-side environment variables schema here. This way you can ensure the app
7 | * isn't built with invalid env vars.
8 | */
9 | server: {
10 | DATABASE_URL: z.string().url(),
11 | NODE_ENV: z
12 | .enum(["development", "test", "production"])
13 | .default("development"),
14 | },
15 |
16 | /**
17 | * Specify your client-side environment variables schema here. This way you can ensure the app
18 | * isn't built with invalid env vars. To expose them to the client, prefix them with
19 | * `NEXT_PUBLIC_`.
20 | */
21 | client: {
22 | // NEXT_PUBLIC_CLIENTVAR: z.string(),
23 | },
24 |
25 | /**
26 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
27 | * middlewares) or client-side so we need to destruct manually.
28 | */
29 | runtimeEnv: {
30 | DATABASE_URL: process.env.DATABASE_URL,
31 | NODE_ENV: process.env.NODE_ENV,
32 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
33 | },
34 | /**
35 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
36 | * useful for Docker builds.
37 | */
38 | skipValidation: !!process.env.SKIP_ENV_VALIDATION,
39 | /**
40 | * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
41 | * `SOME_VAR=''` will throw an error.
42 | */
43 | emptyStringAsUndefined: true,
44 | });
45 |
--------------------------------------------------------------------------------
/src/server/api/root.ts:
--------------------------------------------------------------------------------
1 | import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
2 | import { frameworkRouter } from "./routers/frameworks";
3 |
4 | /**
5 | * This is the primary router for your server.
6 | *
7 | * All routers added in /api/routers should be manually added here.
8 | */
9 | export const appRouter = createTRPCRouter({
10 | framework: frameworkRouter,
11 | });
12 |
13 | // export type definition of API
14 | export type AppRouter = typeof appRouter;
15 |
16 | /**
17 | * Create a server-side caller for the tRPC API.
18 | * @example
19 | * const trpc = createCaller(createContext);
20 | * const res = await trpc.post.all();
21 | * ^? Post[]
22 | */
23 | export const createCaller = createCallerFactory(appRouter);
24 |
--------------------------------------------------------------------------------
/src/server/api/routers/frameworks.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from "@trpc/server";
2 | import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
3 | import { getCounterData } from "~/server/service/counter";
4 | import { getHistoryData } from "~/server/service/history";
5 | import { getRuntimeData } from "~/server/service/runtimes";
6 |
7 | export const frameworkRouter = createTRPCRouter({
8 | getCounter: publicProcedure.query(async () => {
9 | try {
10 | const counterData = await getCounterData();
11 | return counterData;
12 | } catch (error) {
13 | console.log(error);
14 | throw new TRPCError({
15 | code: "INTERNAL_SERVER_ERROR",
16 | message: "Unable to get hidden widget layouts",
17 | });
18 | }
19 | }),
20 | getHistory: publicProcedure.query(async () => {
21 | try {
22 | const counterData = await getHistoryData();
23 | return counterData;
24 | } catch (error) {
25 | console.log(error);
26 | throw new TRPCError({
27 | code: "INTERNAL_SERVER_ERROR",
28 | message: "Unable to get hidden widget layouts",
29 | });
30 | }
31 | }),
32 | getRuntimes: publicProcedure.query(async () => {
33 | try {
34 | const counterData = await getRuntimeData();
35 | return counterData;
36 | } catch (error) {
37 | console.log(error);
38 | throw new TRPCError({
39 | code: "INTERNAL_SERVER_ERROR",
40 | message: "Unable to get hidden widget layouts",
41 | });
42 | }
43 | }),
44 | });
45 |
--------------------------------------------------------------------------------
/src/server/api/trpc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
3 | * 1. You want to modify request context (see Part 1).
4 | * 2. You want to create a new middleware or type of procedure (see Part 3).
5 | *
6 | * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
7 | * need to use are documented accordingly near the end.
8 | */
9 | import { initTRPC } from "@trpc/server";
10 | import superjson from "superjson";
11 | import { ZodError } from "zod";
12 |
13 | import { db } from "~/server/db";
14 |
15 | /**
16 | * 1. CONTEXT
17 | *
18 | * This section defines the "contexts" that are available in the backend API.
19 | *
20 | * These allow you to access things when processing a request, like the database, the session, etc.
21 | *
22 | * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
23 | * wrap this and provides the required context.
24 | *
25 | * @see https://trpc.io/docs/server/context
26 | */
27 | export const createTRPCContext = async (opts: { headers: Headers }) => {
28 | return {
29 | db,
30 | ...opts,
31 | };
32 | };
33 |
34 | /**
35 | * 2. INITIALIZATION
36 | *
37 | * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
38 | * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
39 | * errors on the backend.
40 | */
41 | const t = initTRPC.context().create({
42 | transformer: superjson,
43 | errorFormatter({ shape, error }) {
44 | return {
45 | ...shape,
46 | data: {
47 | ...shape.data,
48 | zodError:
49 | error.cause instanceof ZodError ? error.cause.flatten() : null,
50 | },
51 | };
52 | },
53 | });
54 |
55 | /**
56 | * Create a server-side caller.
57 | *
58 | * @see https://trpc.io/docs/server/server-side-calls
59 | */
60 | export const createCallerFactory = t.createCallerFactory;
61 |
62 | /**
63 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
64 | *
65 | * These are the pieces you use to build your tRPC API. You should import these a lot in the
66 | * "/src/server/api/routers" directory.
67 | */
68 |
69 | /**
70 | * This is how you create new routers and sub-routers in your tRPC API.
71 | *
72 | * @see https://trpc.io/docs/router
73 | */
74 | export const createTRPCRouter = t.router;
75 |
76 | /**
77 | * Middleware for timing procedure execution and adding an articifial delay in development.
78 | *
79 | * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
80 | * network latency that would occur in production but not in local development.
81 | */
82 | const timingMiddleware = t.middleware(async ({ next, path }) => {
83 | const start = Date.now();
84 |
85 | if (t._config.isDev) {
86 | // artificial delay in dev
87 | const waitMs = Math.floor(Math.random() * 400) + 100;
88 | await new Promise((resolve) => setTimeout(resolve, waitMs));
89 | }
90 |
91 | const result = await next();
92 |
93 | const end = Date.now();
94 | console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
95 |
96 | return result;
97 | });
98 |
99 | /**
100 | * Public (unauthenticated) procedure
101 | *
102 | * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
103 | * guarantee that a user querying is authorized, but you can still access user session data if they
104 | * are logged in.
105 | */
106 | export const publicProcedure = t.procedure.use(timingMiddleware);
107 |
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | import { env } from "~/env";
4 |
5 | const createPrismaClient = () =>
6 | new PrismaClient({
7 | log:
8 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
9 | });
10 |
11 | const globalForPrisma = globalThis as unknown as {
12 | prisma: ReturnType | undefined;
13 | };
14 |
15 | export const db = globalForPrisma.prisma ?? createPrismaClient();
16 |
17 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
18 |
--------------------------------------------------------------------------------
/src/server/db/framework.ts:
--------------------------------------------------------------------------------
1 | import { db } from "../db";
2 |
3 | export async function getAll() {
4 | const allFrameWorks = await db.framework.findMany({
5 | orderBy: {
6 | date: "desc", // Order the results by date in descending order
7 | },
8 | });
9 | return allFrameWorks;
10 | }
11 |
12 | export async function getLatest() {
13 | const mostCurrent = await db.framework.findFirst({
14 | orderBy: {
15 | date: "desc", // Order the results by date in descending order
16 | },
17 | });
18 |
19 | if (!mostCurrent) {
20 | throw new Error("No framework data found");
21 | }
22 |
23 | return mostCurrent;
24 | }
25 |
--------------------------------------------------------------------------------
/src/server/db/runtime.ts:
--------------------------------------------------------------------------------
1 | import { db } from "../db";
2 |
3 | export async function getAll() {
4 | const all = await db.runtime.findMany({
5 | orderBy: {
6 | date: "desc", // Order the results by date in descending order
7 | },
8 | });
9 | return all;
10 | }
11 |
--------------------------------------------------------------------------------
/src/server/service/counter.ts:
--------------------------------------------------------------------------------
1 | import { calculateDays } from "~/util/date";
2 | import * as frameworkRepo from "../db/framework";
3 |
4 | export async function getCounterData() {
5 | const latestFramework = await frameworkRepo.getLatest();
6 | const diffDays = calculateDays(latestFramework.date);
7 | const displayDate = latestFramework.date.toLocaleDateString("en-US", {
8 | year: "numeric",
9 | month: "long",
10 | day: "numeric",
11 | });
12 |
13 | return {
14 | count: diffDays,
15 | date: displayDate,
16 | framework: latestFramework,
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/server/service/history.ts:
--------------------------------------------------------------------------------
1 | import { calculateDays } from "~/util/date";
2 | import * as frameworkRepo from "../db/framework";
3 | import { type HistoryData } from "../types";
4 |
5 | export async function getHistoryData(): Promise {
6 | const frameworks = await frameworkRepo.getAll();
7 |
8 | const withDate = frameworks.map((f) => {
9 | const diffDays = calculateDays(f.date);
10 | const displayDate = f.date.toLocaleDateString("en-US", {
11 | year: "numeric",
12 | month: "long",
13 | day: "numeric",
14 | });
15 |
16 | return {
17 | count: diffDays,
18 | date: displayDate,
19 | entity: f,
20 | } satisfies HistoryData;
21 | });
22 |
23 | return withDate;
24 | }
25 |
--------------------------------------------------------------------------------
/src/server/service/runtimes.ts:
--------------------------------------------------------------------------------
1 | import { calculateDays } from "~/util/date";
2 | import * as runtimeRepo from "../db/runtime";
3 | import { type RuntimeData } from "../types";
4 |
5 | export async function getRuntimeData(): Promise {
6 | const data = await runtimeRepo.getAll();
7 |
8 | const withDate = data.map((f) => {
9 | const diffDays = f.date ? calculateDays(f.date) : null;
10 | const displayDate = f.date
11 | ? f.date.toLocaleDateString("en-US", {
12 | year: "numeric",
13 | month: "long",
14 | day: "numeric",
15 | })
16 | : "Unknown";
17 |
18 | return {
19 | count: diffDays,
20 | date: displayDate,
21 | entity: f,
22 | } satisfies RuntimeData;
23 | });
24 |
25 | const sortedByDate = withDate.sort((a, b) => {
26 | if (a.entity.date && b.entity.date) {
27 | return b.entity.date.getTime() - a.entity.date.getTime();
28 | }
29 |
30 | return -1;
31 | });
32 |
33 | return sortedByDate;
34 | }
35 |
--------------------------------------------------------------------------------
/src/server/types.ts:
--------------------------------------------------------------------------------
1 | export type CounterData = {
2 | entity: EntityData;
3 | count: number;
4 | };
5 |
6 | export type EntityData = {
7 | id: number;
8 | name: string;
9 | link: string | null;
10 | releaseLink: string | null;
11 | date: Date;
12 | };
13 |
14 | export type HistoryData = CounterData & {
15 | date: string;
16 | };
17 |
18 | export type RuntimeData = {
19 | count: number | null;
20 | date: string;
21 | entity: Omit & { date: Date | null };
22 | };
23 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/trpc/query-client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultShouldDehydrateQuery,
3 | QueryClient,
4 | } from "@tanstack/react-query";
5 | import SuperJSON from "superjson";
6 |
7 | export const createQueryClient = () =>
8 | new QueryClient({
9 | defaultOptions: {
10 | queries: {
11 | // With SSR, we usually want to set some default staleTime
12 | // above 0 to avoid refetching immediately on the client
13 | staleTime: 30 * 1000,
14 | },
15 | dehydrate: {
16 | serializeData: SuperJSON.serialize,
17 | shouldDehydrateQuery: (query) =>
18 | defaultShouldDehydrateQuery(query) ||
19 | query.state.status === "pending",
20 | },
21 | hydrate: {
22 | deserializeData: SuperJSON.deserialize,
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/trpc/react.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
4 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
5 | import { createTRPCReact } from "@trpc/react-query";
6 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
7 | import { useState } from "react";
8 | import SuperJSON from "superjson";
9 |
10 | import { type AppRouter } from "~/server/api/root";
11 | import { createQueryClient } from "./query-client";
12 |
13 | let clientQueryClientSingleton: QueryClient | undefined = undefined;
14 | const getQueryClient = () => {
15 | if (typeof window === "undefined") {
16 | // Server: always make a new query client
17 | return createQueryClient();
18 | }
19 | // Browser: use singleton pattern to keep the same query client
20 | return (clientQueryClientSingleton ??= createQueryClient());
21 | };
22 |
23 | export const api = createTRPCReact();
24 |
25 | /**
26 | * Inference helper for inputs.
27 | *
28 | * @example type HelloInput = RouterInputs['example']['hello']
29 | */
30 | export type RouterInputs = inferRouterInputs;
31 |
32 | /**
33 | * Inference helper for outputs.
34 | *
35 | * @example type HelloOutput = RouterOutputs['example']['hello']
36 | */
37 | export type RouterOutputs = inferRouterOutputs;
38 |
39 | export function TRPCReactProvider(props: { children: React.ReactNode }) {
40 | const queryClient = getQueryClient();
41 |
42 | const [trpcClient] = useState(() =>
43 | api.createClient({
44 | links: [
45 | loggerLink({
46 | enabled: (op) =>
47 | process.env.NODE_ENV === "development" ||
48 | (op.direction === "down" && op.result instanceof Error),
49 | }),
50 | unstable_httpBatchStreamLink({
51 | transformer: SuperJSON,
52 | url: getBaseUrl() + "/api/trpc",
53 | headers: () => {
54 | const headers = new Headers();
55 | headers.set("x-trpc-source", "nextjs-react");
56 | return headers;
57 | },
58 | }),
59 | ],
60 | })
61 | );
62 |
63 | return (
64 |
65 |
66 | {props.children}
67 |
68 |
69 | );
70 | }
71 |
72 | function getBaseUrl() {
73 | if (typeof window !== "undefined") return window.location.origin;
74 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
75 | return `http://localhost:${process.env.PORT ?? 3000}`;
76 | }
77 |
--------------------------------------------------------------------------------
/src/trpc/server.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import { createHydrationHelpers } from "@trpc/react-query/rsc";
4 | import { headers } from "next/headers";
5 | import { cache } from "react";
6 |
7 | import { createCaller, type AppRouter } from "~/server/api/root";
8 | import { createTRPCContext } from "~/server/api/trpc";
9 | import { createQueryClient } from "./query-client";
10 |
11 | /**
12 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
13 | * handling a tRPC call from a React Server Component.
14 | */
15 | const createContext = cache(() => {
16 | const heads = new Headers(headers());
17 | heads.set("x-trpc-source", "rsc");
18 |
19 | return createTRPCContext({
20 | headers: heads,
21 | });
22 | });
23 |
24 | const getQueryClient = cache(createQueryClient);
25 | const caller = createCaller(createContext);
26 |
27 | export const { trpc: api, HydrateClient } = createHydrationHelpers(
28 | caller,
29 | getQueryClient
30 | );
31 |
--------------------------------------------------------------------------------
/src/util/date.ts:
--------------------------------------------------------------------------------
1 | import { zeroPad } from "./string";
2 |
3 | export function calculateDays(date: Date): number {
4 | // difference in days
5 | const diffTime = Math.abs(new Date().getTime() - date.getTime());
6 | const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
7 |
8 | return diffDays;
9 | }
10 |
11 | export function formatDate(_date: Date | null) {
12 | if (!_date) {
13 | return "Unknown";
14 | }
15 |
16 | const date = new Date(_date);
17 | const year = date.getFullYear();
18 | const month = zeroPad(date.getMonth() + 1, 2);
19 | const day = zeroPad(date.getDate() + 1, 2);
20 | return `${year}/${month}/${day}`;
21 | }
22 |
--------------------------------------------------------------------------------
/src/util/string.ts:
--------------------------------------------------------------------------------
1 | export function zeroPad(num: number, places: number) {
2 | return String(num).padStart(places, "0");
3 | }
4 |
--------------------------------------------------------------------------------
/start-database.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use this script to start a docker container for a local development database
3 |
4 | # TO RUN ON WINDOWS:
5 | # 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
6 | # 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
7 | # 3. Open WSL - `wsl`
8 | # 4. Run this script - `./start-database.sh`
9 |
10 | # On Linux and macOS you can run this script directly - `./start-database.sh`
11 |
12 | DB_CONTAINER_NAME="days-since-last-js-framework-postgres"
13 |
14 | if ! [ -x "$(command -v docker)" ]; then
15 | echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
16 | exit 1
17 | fi
18 |
19 | if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
20 | echo "Database container '$DB_CONTAINER_NAME' already running"
21 | exit 0
22 | fi
23 |
24 | if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
25 | docker start "$DB_CONTAINER_NAME"
26 | echo "Existing database container '$DB_CONTAINER_NAME' started"
27 | exit 0
28 | fi
29 |
30 | # import env variables from .env
31 | set -a
32 | source .env
33 |
34 | DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
35 | DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
36 |
37 | if [ "$DB_PASSWORD" = "password" ]; then
38 | echo "You are using the default database password"
39 | read -p "Should we generate a random password for you? [y/N]: " -r REPLY
40 | if ! [[ $REPLY =~ ^[Yy]$ ]]; then
41 | echo "Please change the default password in the .env file and try again"
42 | exit 1
43 | fi
44 | # Generate a random URL-safe password
45 | DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
46 | sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
47 | fi
48 |
49 | docker run -d \
50 | --name $DB_CONTAINER_NAME \
51 | -e POSTGRES_USER="postgres" \
52 | -e POSTGRES_PASSWORD="$DB_PASSWORD" \
53 | -e POSTGRES_DB=days-since-last-js-framework \
54 | -p "$DB_PORT":5432 \
55 | docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
56 |
--------------------------------------------------------------------------------
/static/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { type Config } from "tailwindcss";
2 |
3 | export default {
4 | content: ["./src/**/*.tsx"],
5 | theme: {},
6 | plugins: [],
7 | } satisfies Config;
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Base Options: */
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "es2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 |
12 | /* Strictness */
13 | "strict": true,
14 | "noUncheckedIndexedAccess": true,
15 | "checkJs": true,
16 |
17 | /* Bundled projects */
18 | "lib": ["dom", "dom.iterable", "ES2022"],
19 | "noEmit": true,
20 | "module": "ESNext",
21 | "moduleResolution": "Bundler",
22 | "jsx": "preserve",
23 | "plugins": [{ "name": "next" }],
24 | "incremental": true,
25 |
26 | /* Path Aliases */
27 | "baseUrl": ".",
28 | "paths": {
29 | "~/*": ["./src/*"]
30 | }
31 | },
32 | "include": [
33 | ".eslintrc.cjs",
34 | "next-env.d.ts",
35 | "**/*.ts",
36 | "**/*.tsx",
37 | "**/*.cjs",
38 | "**/*.js",
39 | ".next/types/**/*.ts"
40 | ],
41 | "exclude": ["node_modules"]
42 | }
43 |
--------------------------------------------------------------------------------