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

Better landing page #420

Merged
merged 21 commits into from
Nov 12, 2024
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
350 changes: 249 additions & 101 deletions frontend/app/landing.tsx

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions frontend/components/landing/faq-answer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { FaqAnswerDetails } from "@/types/landing";

const FaqAnswer = ({ id, title, description }: FaqAnswerDetails) => {
return (
<AccordionItem
className="border rounded-lg px-8 py-2 bg-background text-text"
value={`q${id}`}
>
<AccordionTrigger className="text-left">
<h3 className="text-base lg:text-lg font-medium mr-2">{title}</h3>
</AccordionTrigger>
<AccordionContent className="text-gray-600 text-sm lg:text-base">
{description}
</AccordionContent>
</AccordionItem>
);
};

export default FaqAnswer;
80 changes: 80 additions & 0 deletions frontend/components/landing/feature-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react"; // Icons for expanding/collapsing

interface Feature {
title: string;
description: string;
icon: JSX.Element;
featureRender: JSX.Element;
}

interface FeatureListProps {
features: Feature[];
}

const FeatureList = ({ features }: FeatureListProps) => {
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(
features[0] || null,
);

const handleClick = (feature: Feature) => {
setSelectedFeature(feature === selectedFeature ? null : feature);
};

return (
<div className="flex flex-col md:flex-row items-stretch md:items-center md:gap-8 xl:gap-12 w-full">
{/* List of features with initially-hidden description */}
<div className="grow md:basis-1/3 space-y-3">
{features.map((feature, index) => (
<div
className="w-full max-w-full cursor-pointer border-b pb-2"
key={index}
onClick={() => handleClick(feature)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-2">
{feature.icon}
<h3 className="text-lg font-medium">{feature.title}</h3>
</div>
{selectedFeature === feature ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</div>
{selectedFeature === feature && (
<div className="mt-2 max-w-full">
<p className="text-sm lg:text-base text-gray-600 text-wrap">
{feature.description}
</p>
<div
className="mt-4 flex flex-col w-full items-stretch cursor-default py-4 md:hidden"
id="mobile-demo"
onClick={(e) => e.stopPropagation()}
>
{feature.featureRender}
</div>
</div>
)}
</div>
))}
</div>

{/* Feature demo */}
<div
className="hidden md:grow md:flex md:flex-col md:basis-2/3 md:items-stretch md:justify-center"
id="desktop-demo"
>
{selectedFeature ? (
selectedFeature.featureRender
) : features[0] ? (
features[0].featureRender
) : (
<p className="text-gray-500">Select a feature to view details</p>
)}
</div>
</div>
);
};

export default FeatureList;
110 changes: 110 additions & 0 deletions frontend/components/landing/sample-feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
BadgeHelpIcon,
BookOpenCheckIcon,
LucideIcon,
MessageSquareTextIcon,
SmileIcon,
} from "lucide-react";

import { Inclination } from "@/client";
import LikeButtons from "@/components/likes/like-buttons";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { cn } from "@/lib/utils";
import { SampleComment } from "@/types/landing";

const inclinationToDisplayMap: Record<Inclination, string> = {
good: "Good",
neutral: "Comment",
bad: "Possible improvement",
};

const inclinationToIconMap: Record<Inclination, LucideIcon> = {
good: SmileIcon,
neutral: MessageSquareTextIcon,
bad: BadgeHelpIcon,
};

const inclinationToClassNameMap: Record<Inclination, string> = {
good: "text-green-700",
neutral: "text-blue-500",
bad: "text-amber-600",
};

const inclinationToChipClassNameMap: Record<Inclination, string> = {
good: "bg-green-200/40",
neutral: "bg-blue-100/50",
bad: "bg-amber-200/40",
};

const inclinationToTriggerClassNameMap: Record<Inclination, string> = {
good: "decoration-green-700",
neutral: "decoration-blue-500",
bad: "decoration-amber-600",
};

const SampleFeedback = ({ comments }: { comments: SampleComment[] }) => {
return (
<>
<div className="flex items-center text-lg 2xl:text-2xl px-6 2xl:px-10 pb-2 2xl:pb-0 pt-2 justify-between text-slate-600 mb-2">
<span className="flex items-center">
<BookOpenCheckIcon
className="inline-flex mr-3"
size={22}
strokeWidth={1.6}
/>
<h2 className="font-semibold">Jippy Feedback</h2>
</span>
</div>
<div className="flex flex-col gap-y-6 px-6 2xl:px-10">
<Accordion type="multiple">
{comments.map((comment, index) => {
const CommentIcon = inclinationToIconMap[comment.inclination];
return (
<AccordionItem key={index} value={`comment-${index}`}>
<AccordionTrigger
className={cn(
inclinationToTriggerClassNameMap[comment.inclination],
)}
>
<span
className={cn(
"flex items-center",
inclinationToClassNameMap[comment.inclination],
)}
>
{<CommentIcon className="mr-3" />}
<span
className={cn(
"px-2 py-1 rounded-lg",
inclinationToChipClassNameMap[comment.inclination],
)}
>
{inclinationToDisplayMap[comment.inclination]}
</span>
</span>
</AccordionTrigger>
<AccordionContent className="text-text-muted text-lg">
<div className="px-4">
<p>{comment.content}</p>
<LikeButtons
onDislike={() => {}}
onLike={() => {}}
userLikeValue={0}
/>
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
</>
);
};

export default SampleFeedback;
53 changes: 53 additions & 0 deletions frontend/components/landing/sample-points.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Chip from "@/components/display/chip";
import {
Accordion,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { SamplePoint } from "@/types/landing";

const SamplePoints = ({
question,
points,
}: {
question: string;
points: SamplePoint[];
}) => {
return (
<>
<h1 className="px-8 md:px-0 text-2xl lg:text-3x xl:text-4xl font-semibold text-text mb-5 2xl:mb-6">
{question}
</h1>
<Accordion className="flex flex-col gap-y-4" type="multiple">
{points.map((point, index) => (
<AccordionItem
className="border border-primary/15 rounded-lg px-8 py-2 2xl:px-12 2xl:py-6 bg-background"
key={index}
value={`point-${index}`}
>
<AccordionTrigger
chevronClassName="h-6 w-6 stroke-[2.5] ml-4"
className="text-lg lg:text-xl 2xl:text-2xl text-primary font-medium text-start hover:no-underline pt-4 pb-6"
>
<div className="flex flex-col">
<div className="flex">
<Chip
className="flex mb-4 w-fit max-w-full 2xl:text-xl"
label={point.positive ? "For" : "Against"}
size="lg"
variant={point.positive ? "secondary" : "accent"}
/>
</div>
<span className="inline-block text-primary-900 hover:text-primary-900/80">
{point.title}
</span>
</div>
</AccordionTrigger>
</AccordionItem>
))}
</Accordion>
</>
);
};

export default SamplePoints;
9 changes: 6 additions & 3 deletions frontend/components/news/news-article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
import { articleSourceToDisplayNameMap } from "@/types/events";
import { parseDate, parseDateNoYear } from "@/utils/date";

const NewsArticle = (props: { newsArticle: MiniArticleDTO }) => {
const NewsArticle = (props: {
onClick?: () => void;
newsArticle: MiniArticleDTO;
}) => {
const router = useRouter();

const ASPECT_RATIO = 273 / 154;
Expand All @@ -28,14 +31,14 @@ const NewsArticle = (props: { newsArticle: MiniArticleDTO }) => {
getCategoryFor(category.name),
);

const onClick = () => {
const onClickArticle = () => {
router.push(`/articles/${newsArticle.id}`);
};

return (
<div
className="flex h-full flex-col py-4 w-full lg:py-6 sm:px-4 md:px-8 xl:px-12 xl:py-10 gap-x-28 border-t-[1px] first:border-none lg:border-y-[0px] hover:bg-primary-alt-foreground/[2.5%] lg:rounded-md cursor-pointer"
onClick={onClick}
onClick={props.onClick || onClickArticle}
>
<div className="hidden sm:flex w-full justify-between text-text-muted/90 mt-2">
<span>{articleSourceToDisplayNameMap[newsArticle.source]}</span>
Expand Down
18 changes: 18 additions & 0 deletions frontend/components/ui/highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FC, ReactNode } from "react";

interface HighlightProps {
children: ReactNode;
color?: string;
}

const Highlight: FC<HighlightProps> = ({ children, color = "bg-primary" }) => {
return (
<span
className={`${color} px-1 rounded-md inline align-baseline text-white`}
>
{children}
</span>
);
};

export default Highlight;
17 changes: 17 additions & 0 deletions frontend/types/landing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Inclination } from "@/client";

export interface FaqAnswerDetails {
id: number;
title: string;
description: string;
};

export interface SampleComment {
inclination: Inclination;
content: string;
};

export interface SamplePoint {
positive: boolean;
title: string;
};
Loading