Skip to content

Commit

Permalink
Added a specific loader to each suspended server compoentn. (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
JurreBrandsenInfoSupport authored Mar 22, 2024
1 parent 0364236 commit 5ea8d1e
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 71 deletions.
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ModeToggle } from "~/components/mode-toggle";
import { Login } from "~/components/login";
import { type Session } from "next-auth";
import { getServerAuthSession } from "~/server/auth";
import ButtonSkeleton from "~/components/loading/button-loader";

const inter = Inter({
subsets: ["latin"],
Expand Down Expand Up @@ -50,7 +51,7 @@ export default async function RootLayout({
<main className="flex min-h-screen items-center justify-center">
<div className="absolute right-4 top-4 z-50 flex items-center space-x-4">
{session && (
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<ButtonSkeleton />}>
<LoginWrapper session={session} />
</Suspense>
)}
Expand Down
57 changes: 0 additions & 57 deletions src/app/loading.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import SelectRole from "../components/select-role";
import React, { Suspense } from "react";
import { type Session } from "next-auth";
import { db } from "~/server/db";
import RoleSelectionSkeleton from "~/components/loading/role-selection-loader";

const Home: React.FC = async () => {
const session = await getServerAuthSession();

// Use Suspense to suspend rendering while the data is being fetched
return (
<div>
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
Expand Down Expand Up @@ -44,7 +44,7 @@ const Home: React.FC = async () => {
{/* If the user is logged in, show the SelectRole component */}
{session && (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<RoleSelectionSkeleton />}>
<SelectRoleWrapper session={session} />
</Suspense>
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/app/result/[role]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { slugify } from "~/utils/slugify";
import ResultsWrapper from "~/components/results";

import { type Metadata } from "next";
import ButtonSkeleton from "~/components/loading/button-loader";
import LegendSkeleton from "~/components/loading/results-loader";

export const metadata: Metadata = {
title: "Results",
Expand All @@ -30,11 +32,11 @@ const Results: React.FC = async () => {
</span>
<span className="block sm:inline"> Tech Survey - Results</span>
</h1>
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<ButtonSkeleton />}>
<ShowRolesWrapper />
</Suspense>

<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<LegendSkeleton />}>
<ShowResultsWrapper />
</Suspense>
{!session && (
Expand Down
4 changes: 2 additions & 2 deletions src/app/survey/[role]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getServerAuthSession } from "~/server/auth";
import { type AnswerOption, type Question } from "~/models/types";
import { Suspense } from "react";
import { SurveyQuestionnaire } from "~/components/survey-questionnaire";
import Loading from "~/app/loading";
import SurveyQuestionLoader from "~/components/loading/survey-question-loader";
import { db } from "~/server/db";

import { type Metadata } from "next";
Expand All @@ -12,7 +12,7 @@ export const metadata: Metadata = {
};

const SuspenseSurveyData = () => (
<Suspense fallback={<Loading />}>
<Suspense fallback={<SurveyQuestionLoader />}>
<SurveyPage />
</Suspense>
);
Expand Down
3 changes: 2 additions & 1 deletion src/app/thank-you/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "~/models/types";

import { type Metadata } from "next";
import ButtonSkeleton from "~/components/loading/button-loader";

export const metadata: Metadata = {
title: "Thank You",
Expand Down Expand Up @@ -88,7 +89,7 @@ const ThankYou = async () => {
We appreciate your time and effort in completing the survey.
</p>
<div className="w-full max-w-3xl">
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<ButtonSkeleton />}>
<PdfDownloadButton
userAnswersForRole={transformedData}
answerOptions={answerOptions}
Expand Down
9 changes: 9 additions & 0 deletions src/components/loading/button-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const ButtonSkeleton = () => {
return (
<button className="inline-block cursor-not-allowed rounded-md bg-gray-300 px-16 py-5 text-lg font-semibold text-gray-300 opacity-50 transition-opacity duration-300 ease-in-out hover:opacity-50 focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-opacity-50">
<span className="block h-full w-full animate-pulse" />
</button>
);
};

export default ButtonSkeleton;
49 changes: 49 additions & 0 deletions src/components/loading/progression-bar-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Fragment } from "react";
import { type Role } from "~/models/types";

const NavigationSkeleton = ({ roles }: { roles: Role[] }) => {
return (
<nav className="relative mt-8 p-4" style={{ padding: "100px 40px 40px" }}>
<div className="mt-4">
{/* Progress bar */}
<div
className="flex items-center justify-between"
style={{ width: "90%" }}
>
{roles.map((section, index) => (
<Fragment key={section.id}>
{/* Circle with clickable area */}
<div className="relative flex items-center justify-center">
<div
className={`mb-1 h-6 w-6 animate-pulse rounded-full border-2 bg-gray-300`}
/>
<div
className="absolute -rotate-45 whitespace-nowrap text-xs font-semibold"
style={{
transformOrigin: "bottom left",
top: "-22px",
left: "calc(100% - 0px)",
}}
>
<span className="block h-4 w-16 animate-pulse bg-gray-300" />
</div>
</div>
{/* Line (except for the last section) */}
{index !== roles.length - 1 && (
<div
className={`mx-2 h-0.5 flex-1 animate-pulse bg-gray-300`}
/>
)}
</Fragment>
))}
</div>
</div>
<div className="flex items-center justify-between">
<h3 className="hidden animate-pulse text-lg font-semibold">Progress</h3>
<span className="animate-pulse text-sm">0.00% Completed</span>
</div>
</nav>
);
};

export default NavigationSkeleton;
41 changes: 41 additions & 0 deletions src/components/loading/results-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client";

import { ResponsiveContainer } from "recharts";

const LegendSkeleton = () => {
return (
<div className="grid gap-4">
<h2 className="mb-4 animate-pulse text-lg font-semibold">Legend</h2>
<div className="flex flex-wrap gap-4">
{[...Array<number>(5)].map((_, index) => (
<div key={index} className="flex items-center">
<div className="mr-4 h-6 w-6 animate-pulse rounded-full bg-gray-300"></div>
<span className="h-6 w-24 animate-pulse bg-gray-300"></span>
</div>
))}
</div>
{[...Array<number>(3)].map((_, index) => (
<div key={index} className="grid gap-4 md:grid-cols-2">
<div className="rounded-md p-4 shadow-md">
<h3 className="mb-4 animate-pulse text-lg font-medium">Question</h3>
<div className="chart-container">
<ResponsiveContainer width="100%" height={300}>
<div className="h-full w-full animate-pulse bg-gray-300"></div>
</ResponsiveContainer>
</div>
</div>
<div className="rounded-md p-4 shadow-md">
<h3 className="mb-4 animate-pulse text-lg font-medium">Question</h3>
<div className="chart-container">
<ResponsiveContainer width="100%" height={300}>
<div className="h-full w-full animate-pulse bg-gray-300"></div>
</ResponsiveContainer>
</div>
</div>
</div>
))}
</div>
);
};

export default LegendSkeleton;
27 changes: 27 additions & 0 deletions src/components/loading/role-selection-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ButtonSkeleton from "./button-loader";

const RoleSelectionSkeleton = () => {
return (
<div className="container mx-auto py-8">
<h1 className="mb-4 animate-pulse text-2xl font-bold">Select Roles</h1>
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 15 }).map((_, index) => (
<li
key={index}
className={`cursor-not-allowed rounded-lg border p-4 opacity-50 hover:bg-gray-100 hover:bg-opacity-25`}
>
<div className="flex items-center">
<div className="mr-2 h-5 w-5 animate-pulse rounded-full bg-gray-300"></div>
<div className="h-4 w-20 animate-pulse bg-gray-300"></div>
</div>
</li>
))}
</ul>
<div className="mt-8 flex">
<ButtonSkeleton />
</div>
</div>
);
};

export default RoleSelectionSkeleton;
59 changes: 59 additions & 0 deletions src/components/loading/survey-question-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Skeleton } from "~/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import NavigationSkeleton from "./progression-bar-loader";

export default function SurveyQuestionLoader() {
const answerOptions = [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }];

// create 6 roles of type Role[]
const roles = Array.from({ length: 6 }, (_, index) => ({
id: index.toString(),
role: `Role ${index}`,
default: index === 0,
}));

return (
<div>
<NavigationSkeleton roles={roles} />
<div className="container flex h-full flex-col items-center justify-center gap-12 px-4 py-16">
<div>
<Table>
<TableHeader className="sticky top-0 z-10 h-10 w-full bg-slate-100 dark:bg-slate-900">
<TableRow>
<TableHead className="w-[400px]">
<Skeleton className="h-4 w-full" />
</TableHead>
{answerOptions.map((option) => (
<TableHead key={option.id}>
<Skeleton className="h-4 w-full" />
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 20 }).map((_, index) => (
<TableRow key={index}>
<TableCell>
<Skeleton className="h-4 w-[250px]" />
</TableCell>
{answerOptions.map((option) => (
<TableCell className="h-[40px]" key={option.id}>
<Skeleton className="h-4 w-[300px] " />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</div>
);
}
13 changes: 8 additions & 5 deletions src/components/results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { type TransformedData } from "~/models/types";
import ShowResults from "./show-results";
import { idToTextMap } from "~/utils/optionMapping";

export default function ResultsWrapper({ data }: { data: TransformedData }) {
return (
Expand Down Expand Up @@ -51,11 +52,13 @@ export function ResultCommons({ data }: { data: TransformedData }) {
style={customTooltipStyle}
>
<p className="label dark:text-black">
{payload.map((entry, index) => (
<span key={`item-${index}`}>
{`${entry.name} : ${entry.value}`}
</span>
))}
{payload.map(
(entry: { name: string; value: number }, index: number) => (
<span key={`item-${index}`}>
{`${idToTextMap[+entry.name]} : ${entry.value}`}
</span>
),
)}
</p>
</div>
</div>
Expand Down
Loading

0 comments on commit 5ea8d1e

Please sign in to comment.