Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Member page app router + tailwind #2039

Merged
merged 7 commits into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/web/app/api/member/[id]/documents/[page]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import getMemberDocuments from "@lib/api/documents/get-member-documents";

interface Payload {
params: {
id: string;
page: string;
};
}

export async function GET(
_request: Request,
{ params: { id, page } }: Payload,
) {
const pageInt = parseInt(page);

if (Number.isNaN(pageInt)) {
return new Response("Invalid page", { status: 400 });
}

const memberDocuments = await getMemberDocuments({ id, page: pageInt });
return Response.json(memberDocuments);
}
2 changes: 1 addition & 1 deletion apps/web/app/cookie-policy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Card } from "@components/card";
import { Card } from "@components/common/card";
import Container from "@components/common/container";
import PageTitle from "@components/common/page-title";
import { CodeBracketIcon } from "@heroicons/react/24/solid";
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/error.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { PrimaryButton } from "@components/button";
import { Card } from "@components/card";
import { PrimaryButton } from "@components/common/button";
import { Card } from "@components/common/card";
import Container from "@components/common/container";
import PageTitle from "@components/common/page-title";
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
Expand Down
6 changes: 5 additions & 1 deletion apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Head from "./head";
import Footer from "./footer";
import Header from "@components/header/header";
import { ThemeProvider } from "@components/providers/theme-provider";
import { twMerge } from "tailwind-merge";

const roboto = Roboto({
weight: ["300", "400", "500", "700"],
Expand All @@ -18,7 +19,10 @@ export default function RootLayout({ children }: PropsWithChildren) {
<html lang="sv" suppressHydrationWarning>
<Head />
<body
className={`${roboto.className} bg-background-light dark:bg-background-dark text-font-light dark:text-font-dark flex min-h-screen flex-col shadow-sm`}
className={twMerge(
roboto.className,
"bg-background-light dark:bg-background-dark text-font-light dark:text-font-dark flex min-h-screen flex-col",
)}
>
<ThemeProvider attribute="class">
<Header />
Expand Down
33 changes: 33 additions & 0 deletions apps/web/app/ledamot/[id]/biography-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { ChevronDownIcon } from "@heroicons/react/24/solid";
import type { Information } from "@lib/api/member/types";
import { useState } from "react";

interface Props {
information: Information;
}

export default function BiographyEntry({ information }: Props) {
const [isOpen, setIsOpen] = useState(false);

function toggleEntry() {
setIsOpen((prevState) => !prevState);
}

return (
<button
className="group w-full border-t-2 border-slate-300 text-left dark:border-slate-600"
onClick={toggleEntry}
aria-expanded={isOpen}
>
<div className="flex items-center justify-between p-4">
{information.code}
<ChevronDownIcon className="h-4 w-4 transition-transform group-aria-[expanded=true]:rotate-180" />
</div>
<p className="px-4 pb-6 pt-2 group-aria-[expanded=false]:hidden">
{information.content}
</p>
</button>
);
}
27 changes: 27 additions & 0 deletions apps/web/app/ledamot/[id]/biography.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Card } from "@components/common/card";
import type { Information } from "@lib/api/member/types";
import BiographyEntry from "./biography-entry";

interface Props {
memberInformation: Information[];
}

export default function Biography({ memberInformation }: Props) {
if (memberInformation.length === 0) {
return null;
}

return (
<Card className="p-0">
<h4 className="p-4 text-xl sm:text-2xl">Biografi</h4>
<div>
{memberInformation.map((info) => (
<BiographyEntry
key={`${info.type}:${info.code}`}
information={info}
/>
))}
</div>
</Card>
);
}
73 changes: 73 additions & 0 deletions apps/web/app/ledamot/[id]/documents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";
import { Card, CommitteeHeader } from "@components/common/card";
import Pagination from "@components/common/pagination";
import type { MemberDocument, MemberDocuments } from "@lib/api/member/types";
import { routes } from "@lib/navigation";
import Link from "next/link";
import { useRef, useState } from "react";

interface DocumentProps {
document: MemberDocument;
}

function Document({ document }: DocumentProps) {
return (
<Link href={routes.document(document.id)}>
<Card className="p-0 transition-opacity hover:opacity-75 dark:shadow-slate-900">
<CommitteeHeader committee={document.committee} />
<div className="p-4 ">
<p className="text-sm text-slate-600 dark:text-slate-400">
{document.title}
</p>
<p className="pb-1">{document.altTitle}</p>
<p className="text-sm text-slate-600 dark:text-slate-400">
{document.subtitle}
</p>
</div>
</Card>
</Link>
);
}

interface Props {
memberId: string;
initialDocuments: MemberDocuments;
}

export default function Documents({ initialDocuments, memberId }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [page, setPage] = useState(1);
const [documents, setDocuments] = useState(initialDocuments.documents);

async function changePage(page: number) {
const response = await fetch(
`${window.location.origin}${routes.api.memberDocument(memberId, page)}`,
{ next: { revalidate: 60 * 60 * 24 } },
);
const newDocuments: MemberDocuments = await response.json();
setPage(page);
setDocuments(newDocuments.documents);
containerRef.current?.scrollIntoView();
}

return (
<div
className="scroll-mt-header-with-margin sm:scroll-mt-header-sm-with-margin m-4 grid gap-4"
ref={containerRef}
>
<Pagination
current={page}
total={initialDocuments.pages}
onChange={changePage}
/>
{documents.map((document) => (
<Document key={document.id} document={document} />
))}
<Pagination
current={page}
total={initialDocuments.pages}
onChange={changePage}
/>
</div>
);
}
49 changes: 44 additions & 5 deletions apps/web/app/ledamot/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import getMember from "@lib/api/member/get-member";
import getMembers from "@lib/api/member/get-members";
import { ERROR_404_TITLE } from "@lib/constants";
import { notFound } from "next/navigation";
import Profile from "./profile";
import BreadcrumbsSocialMediaShare from "@components/common/breadcrumbs-social-media-share";
import { routes } from "@lib/navigation";
import Container from "@components/common/container";
import Statistics from "./statistics";
import getMemberWithAbsence from "@lib/api/member/get-member-with-absence";
import getMemberDocuments from "@lib/api/documents/get-member-documents";
import Biography from "./biography";
import Tabs from "./tabs";
import getMemberTwitterFeed from "@lib/api/wikidata/get-member-twitter-feed";

interface PageProps {
params: {
Expand Down Expand Up @@ -29,15 +39,44 @@ export async function generateMetadata({ params: { id } }: PageProps) {
}

export default async function MemberPage({ params: { id } }: PageProps) {
const member = await getMember(id);
const memberPromise = getMemberWithAbsence(id);
const memberDocumentsPromise = getMemberDocuments({ id, page: 1 });
const memberTwitterPromise = getMemberTwitterFeed(id);
const [member, memberDocuments, memberTwitter] = await Promise.all([
memberPromise,
memberDocumentsPromise,
memberTwitterPromise,
]);

if (!member) {
return notFound();
}
// TODO: Implement

return (
<h1>
{member.firstName} {member.lastName}
</h1>
<main>
<Profile member={member} />
<Container className="grid gap-4">
<BreadcrumbsSocialMediaShare
breadcrumbsProps={{
current: `${member.firstName} ${member.lastName}`,
links: [{ title: "Ledamöter", href: routes.members }],
}}
socialMediaProps={{
title: `${member.firstName} ${member.lastName}`,
}}
/>
<Statistics
absence={member.absence}
documentCount={memberDocuments.count}
/>
<Biography memberInformation={member.information} />
<Tabs
memberId={member.id}
initialDocuments={memberDocuments}
twitterFeed={memberTwitter.results.bindings[0]}
/>
</Container>
</main>
);
}

Expand Down
27 changes: 27 additions & 0 deletions apps/web/app/ledamot/[id]/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import MemberImage from "@components/parliament/member-image";
import type { MemberResponse } from "@lib/api/member/types";

interface ProfileProps {
member: MemberResponse;
}

export default function Profile({ member }: ProfileProps) {
return (
<div className="grid justify-center gap-1 text-center">
<div className="bg-primary dark:bg-primary-dark absolute h-28 w-full sm:h-36" />
<MemberImage
member={member}
className="mx-auto mt-8 h-40 w-40 sm:h-56 sm:w-56"
sizes="(min-width: 640px) 14rem, 10rem"
/>
<div className="mt-4 text-lg font-medium sm:text-xl">
{member.status}
{member.isLeader && " och partiledare"}
</div>
<h1 className="text-2xl sm:text-3xl">
{member.firstName} {member.lastName}
</h1>
<div className="text-md sm:text-lg">{member.age} år</div>
</div>
);
}
51 changes: 51 additions & 0 deletions apps/web/app/ledamot/[id]/statistics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Card } from "@components/common/card";
import type { MemberDetailedResponse } from "@lib/api/member/types";
import { twMerge } from "tailwind-merge";

interface StatisticsCardProps {
value: string;
description: string;
className?: string;
}
function StatisticsCard({
value,
description,
className,
}: StatisticsCardProps) {
return (
<Card className={twMerge("flex-1 text-center", className)}>
<div className="text-lg">{value}</div>
<div className="text-sm">{description}</div>
</Card>
);
}

interface Props {
absence: MemberDetailedResponse["absence"];
documentCount: number;
}

export default function Statistics({ absence, documentCount }: Props) {
return (
<div className="flex flex-wrap gap-4">
{absence.mandatePeriod.value !== null && (
<StatisticsCard
value={`${absence.mandatePeriod.value}%`}
description={`Voteringsnärvaro mandatperiod ${absence.mandatePeriod.description}`}
/>
)}
{absence.parliamentYear.value !== null && (
<StatisticsCard
value={`${absence.parliamentYear.value}%`}
description={`Voteringsnärvaro riksmöte ${absence.parliamentYear.description}`}
className="xs:basis-full sm:basis-0"
/>
)}
<StatisticsCard
value={documentCount.toString()}
description="Dokument"
className="basis-full sm:basis-0"
/>
</div>
);
}
Loading