Skip to content

Commit ec18a4d

Browse files
authored
Merge branch 'main' into issue-12-Create_Contact_Form
2 parents 2d62584 + 5535580 commit ec18a4d

38 files changed

Lines changed: 1227 additions & 27 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,5 @@ dist
291291
# misc
292292
.DS_Store
293293

294-
opt/
294+
opt/
295+
server/media/

client/declarations.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,13 @@ declare module "*.module.css" {
22
const classes: { [key: string]: string };
33
export default classes;
44
}
5+
6+
declare module "*.png" {
7+
const content: {
8+
src: string;
9+
height: number;
10+
width: number;
11+
blurDataURL?: string;
12+
};
13+
export default content;
14+
}

client/next.config.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ const isWindowsDevContainer = () =>
88
/** @type {import('next').NextConfig} */
99
const nextConfig = {
1010
reactStrictMode: true,
11+
images: {
12+
remotePatterns: [
13+
// Local development only. Replace this with the production media/CDN
14+
// hostname before deployment.
15+
{
16+
protocol: "http",
17+
hostname: "localhost",
18+
port: "8000",
19+
pathname: "/media/**",
20+
},
21+
],
22+
},
1123
// dumb fix for windows docker
1224
webpack: isWindowsDevContainer()
1325
? (config) => {
136 KB
Loading

client/src/components/content-card.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface ContentCardProps {
1414
buttonLabel?: string;
1515
author?: string;
1616
dateTime?: string;
17+
layout?: "vertical" | "horizontal";
1718
}
1819

1920
export function ContentCard({
@@ -26,19 +27,39 @@ export function ContentCard({
2627
buttonLabel = "View more",
2728
author,
2829
dateTime,
30+
layout = "vertical",
2931
}: ContentCardProps) {
3032
const hasAuthor = Boolean(author);
3133
const hasDateTime = Boolean(dateTime);
34+
const isVerticalLayout = layout === "vertical";
35+
let layoutClass: string;
36+
37+
if (isVerticalLayout) {
38+
if (hasDateTime) {
39+
layoutClass = "aspect-[10/13] max-w-[clamp(18rem,30vw,24rem)]";
40+
} else {
41+
layoutClass = "aspect-[10/11] max-w-[clamp(18rem,30vw,24rem)]";
42+
}
43+
} else {
44+
layoutClass = "flex-row aspect-[9/2] max-w-none";
45+
}
3246

3347
return (
3448
<article
3549
className={cn(
36-
"group flex aspect-[10/13] h-full w-full max-w-[clamp(18rem,30vw,24rem)] flex-col overflow-hidden rounded-[var(--radius)] border border-border/60 bg-[linear-gradient(0deg,rgba(255,255,255,0.05)_0%,rgba(255,255,255,0.05)_100%),linear-gradient(0deg,rgba(255,255,255,1)_0%,rgba(255,255,255,1)_100%)] text-card-foreground shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg",
37-
hasDateTime ? "aspect-[10/13]" : "aspect-[10/11]",
50+
"group flex h-full w-full flex-col overflow-hidden rounded-[var(--radius)] border border-border/60 bg-[linear-gradient(0deg,rgba(255,255,255,0.05)_0%,rgba(255,255,255,0.05)_100%),linear-gradient(0deg,rgba(255,255,255,1)_0%,rgba(255,255,255,1)_100%)] text-card-foreground shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg",
51+
layoutClass,
3852
className,
3953
)}
4054
>
41-
<div className="relative w-full flex-[0_0_55%] overflow-hidden bg-muted">
55+
<div
56+
className={cn(
57+
"relative overflow-hidden bg-muted",
58+
layout === "vertical"
59+
? "w-full flex-[0_0_50%]"
60+
: "h-full w-[45%] shrink-0",
61+
)}
62+
>
4263
<div className="absolute inset-0" />
4364
{imageSrc ? (
4465
<Image
@@ -59,7 +80,7 @@ export function ContentCard({
5980
<div className="absolute inset-0 bg-background/5" />
6081
</div>
6182

62-
<div className="flex flex-1 flex-col gap-[clamp(0.5rem,1.2vw,0.75rem)] p-[clamp(0.5rem,1.2vw,0.75rem)]">
83+
<div className="flex flex-1 flex-col gap-[clamp(0.5rem,1.2vw,0.75rem)] px-[clamp(0.8rem,1.5vw,1.6rem)] py-[clamp(0.4rem,1.0vw,1.2rem)]">
6384
{hasDateTime ? (
6485
<div className="items-row flex items-center gap-2">
6586
<Calendar size={16} className="text-muted-foreground" />
@@ -71,7 +92,14 @@ export function ContentCard({
7192

7293
<div className="flex min-h-0 flex-1 flex-col justify-between overflow-hidden">
7394
<div className="max-h-[80%] space-y-2 overflow-hidden">
74-
<h3 className="overflow-hidden text-[clamp(1rem,2.3vw,1.25rem)] font-semibold leading-[clamp(1.35,2.1vw,1.75)] tracking-tight text-foreground [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]">
95+
<h3
96+
className={cn(
97+
"overflow-hidden font-semibold leading-[clamp(1.35,2.1vw,1.75)] tracking-tight text-foreground [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]",
98+
isVerticalLayout
99+
? "text-[clamp(1rem,2.3vw,1.25rem)]"
100+
: "text-[clamp(1.25rem,2.0vw,2.125rem)]",
101+
)}
102+
>
75103
{title}
76104
</h3>
77105
<p className="overflow-hidden text-[clamp(0.75rem,1.7vw,0.875rem)] leading-[clamp(1.25rem,2.2vw,1.5rem)] text-muted-foreground [-webkit-box-orient:vertical] [-webkit-line-clamp:3] [display:-webkit-box]">

client/src/components/ui/card.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Image, { StaticImageData } from "next/image";
2+
3+
import styles from "@/styles/components/card.module.css";
4+
5+
/**
6+
* Interface that contains fields for a card.
7+
*/
8+
export interface CardProp {
9+
image: StaticImageData;
10+
alt: string;
11+
title: string;
12+
body: string;
13+
}
14+
15+
/** A Placeholder Card component for the front page.
16+
*
17+
* @param param0 CardProp which contain fields for the card
18+
* @returns
19+
*/
20+
export default function Card({ image, alt, title, body }: CardProp) {
21+
return (
22+
<div className={styles["card"]}>
23+
<div className={styles["card-img"]}>
24+
<Image src={image} alt={alt} />
25+
</div>
26+
<div className={styles["card-body"]}>
27+
<h1 className="text-4xl font-bold text-gray-900">{title}</h1>
28+
<p>{body}</p>
29+
</div>
30+
</div>
31+
);
32+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useRef } from "react";
2+
3+
import styles from "@/styles/components/card_container.module.css";
4+
5+
import { ContentCard, ContentCardProps } from "../content-card";
6+
7+
/** Horizontally scrolling container for cards, On widescreen resolutions, scrolls with side buttons
8+
* mobile scrolling is done with pointer.
9+
*
10+
* @param param0 An Array of ContentCardProps which represents the cards to be displayed
11+
* @returns
12+
*/
13+
export default function CardContainer({
14+
cards,
15+
}: {
16+
cards: Array<ContentCardProps>;
17+
}) {
18+
const containerRef = useRef<HTMLDivElement>(null);
19+
20+
const scroll = (direction: "left" | "right") => {
21+
if (containerRef.current) {
22+
containerRef.current.scrollBy({
23+
left: direction === "right" ? 400 : -400,
24+
behavior: "smooth",
25+
});
26+
}
27+
};
28+
29+
return (
30+
<div className={styles["wrapper"]}>
31+
<button className={styles["arrow"]} onClick={() => scroll("left")}>
32+
&#8592;
33+
</button>
34+
<div className={styles["card-container"]} ref={containerRef}>
35+
{cards.map((card_prop) => (
36+
<ContentCard
37+
key={card_prop.href}
38+
{...card_prop}
39+
className="w-[clamp(18rem,30vw,24rem)] shrink-0 snap-start"
40+
/>
41+
))}
42+
</div>
43+
<button className={styles["arrow"]} onClick={() => scroll("right")}>
44+
&#8594;
45+
</button>
46+
</div>
47+
);
48+
}

client/src/hooks/apiService.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
2+
3+
import api from "@/lib/api";
4+
5+
export interface Resource {
6+
id: string;
7+
image: string | null;
8+
name: string;
9+
date_made: string;
10+
author: string;
11+
slug: string;
12+
summary: string;
13+
type: "page" | "file";
14+
body: string;
15+
file_url: string | null;
16+
file_name: string | null;
17+
}
18+
19+
export const getResources = (
20+
args?: Omit<UseQueryOptions<Resource[]>, "queryKey" | "queryFn">,
21+
) => {
22+
return useQuery<Resource[], Error>({
23+
...args,
24+
queryKey: ["resources"],
25+
queryFn: async () => {
26+
const res = await api.get("resources/collection/");
27+
return res.data as Resource[];
28+
},
29+
});
30+
// TODO: Error handling
31+
};
32+
33+
export async function fetchResourceFromSlug(slug: string): Promise<Resource[]> {
34+
const res = await api.get("resources/collection/", {
35+
params: {
36+
slug,
37+
},
38+
});
39+
40+
return res.data as Resource[];
41+
} //had to create to use in GetServerSideProps since it needs to be async since it runs outside react
42+
43+
export function getResourceFromSlug(slug: string) {
44+
return useQuery<Resource[], Error>({
45+
queryKey: ["resources", slug],
46+
queryFn: async () => {
47+
try {
48+
const res = await fetchResourceFromSlug(slug);
49+
return res as Resource[];
50+
} catch (error) {
51+
throw new Error("Failed to load"); //create instance of error
52+
}
53+
},
54+
enabled: !!slug,
55+
});
56+
}

client/src/pages/_app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Navbar from "@/components/ui/Navbar";
99

1010
const montserrat = Montserrat({
1111
subsets: ["latin"],
12-
variable: "--font-sans",
12+
variable: "--font-montserrat",
1313
weight: ["400", "500", "600", "700", "800"],
1414
});
1515

0 commit comments

Comments
 (0)