Skip to content

Commit

Permalink
Merge pull request #23 from Redot-Engine/feature/legal-page
Browse files Browse the repository at this point in the history
Add dynamic contact, FAQ, and license pages
  • Loading branch information
charlottewiltshire0 authored Jan 4, 2025
2 parents 01460d7 + 3441400 commit 5d2f89a
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 37 deletions.
83 changes: 83 additions & 0 deletions app/(root)/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import HeaderSection from "@/components/header-section";
import { ContactCard } from "@/components/contact/contact-card";
import { contactCardsData } from "@/constants/contactCardsData";
import { Start } from "@/components/sections/landing/start";
import { useInView } from "react-intersection-observer";
import { motion } from "motion/react";
import { faqList } from "@/constants/faq";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useTranslations } from "next-intl";

export default function Contact() {
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: true,
});

const cardVariants = {
hidden: { opacity: 0, scale: 0.9 },
visible: { opacity: 1, scale: 1 },
};

const t = useTranslations("contact");

return (
<div ref={ref}>
<div className="relative flex w-full items-start justify-center bg-white bg-grid-black/[0.1] dark:bg-black dark:bg-grid-white/[0.1]">
{/* Radial gradient for the container to give a faded look */}
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black"></div>
<div className="relative z-20 px-5 pb-5 pt-10 lg:px-40">
<HeaderSection section="contact" />
</div>
</div>
<div className="mt-10 px-5 lg:px-40">
<div className="grid grid-rows-4 gap-8 md:grid-cols-2 md:grid-rows-2 lg:grid-cols-4 lg:grid-rows-1">
{contactCardsData.map((card, index) => (
<motion.div
key={index}
initial="hidden"
animate={inView ? "visible" : "hidden"}
variants={cardVariants}
transition={{
duration: 0.5,
delay: index * 0.2,
}}
>
<ContactCard
icon={card.icon}
title={t(`contactCard.${card.title}`)}
description={t(`contactCard.${card.description}`)}
links={card.links}
/>
</motion.div>
))}
</div>
</div>
<div className="mt-24 px-5 lg:px-40">
<div className="flex flex-col items-center gap-8">
<h2 className="mt-5 text-center text-3xl font-bold tracking-tighter md:text-4xl">
{t("faqTitle")}
</h2>
<Accordion type="single" className="w-full md:w-[600px]" collapsible>
{faqList.map((faq, index) => (
<AccordionItem key={index} value={`item-${index + 1}`}>
<AccordionTrigger className="text-left">
{t(`faq.${faq.question}`)}
</AccordionTrigger>
<AccordionContent> {t(`faq.${faq.answer}`)}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
<Start />
</div>
);
}
78 changes: 78 additions & 0 deletions app/(root)/licenses/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import HeaderSection from "@/components/header-section";
import { useEffect, useState } from "react";
import axios from "axios";
import { Start } from "@/components/sections/landing/start";

export default function License() {
const [files, setFiles] = useState<{ [key: string]: string }>({
LICENSE: "",
COPYRIGHT: "",
LOGO_LICENSE: "",
});

useEffect(() => {
const fileUrls = {
LICENSE:
"https://raw.githubusercontent.com/Redot-Engine/redot-engine/refs/heads/master/LICENSE.txt",
COPYRIGHT:
"https://raw.githubusercontent.com/Redot-Engine/redot-engine/refs/heads/master/COPYRIGHT.txt",
LOGO_LICENSE:
"https://raw.githubusercontent.com/Redot-Engine/redot-engine/refs/heads/master/LOGO_LICENSE.txt",
};

const fetchFiles = async () => {
try {
const responses = await Promise.all(
Object.entries(fileUrls).map(async ([key, url]) => {
const response = await axios.get(url);
return { key, content: response.data };
})
);
const newFiles = responses.reduce(
(acc, { key, content }) => {
acc[key] = content;
return acc;
},
{} as { [key: string]: string }
);

setFiles(newFiles);
} catch (error) {
console.error("Error fetching files:", error);
}
};

fetchFiles();
}, []);

return (
<div>
<div className="relative flex w-full items-start justify-center bg-white bg-grid-black/[0.1] dark:bg-black dark:bg-grid-white/[0.1]">
{/* Radial gradient for the container to give a faded look */}
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black"></div>
<div className="relative z-20 px-5 pb-5 pt-10 lg:px-40">
<HeaderSection section="licenses" />
</div>
</div>

<div className="mt-24 flex flex-col gap-8 px-5 lg:px-40">
<div className="flex flex-col gap-4">
<h3 className="text-xl font-medium">LICENSE</h3>
<pre>{files.LICENSE}</pre>
</div>
<div className="flex flex-col gap-4">
<h3 className="text-xl">COPYRIGHT</h3>
<pre>{files.COPYRIGHT}</pre>
</div>
<div className="flex flex-col gap-4">
<h3 className="text-xl">LOGO LICENSE</h3>
<pre>{files.LOGO_LICENSE}</pre>
</div>
</div>

<Start />
</div>
);
}
Binary file modified bun.lockb
Binary file not shown.
51 changes: 51 additions & 0 deletions components/contact/contact-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Link from "next/link";
import React from "react";
import { cn } from "@/lib/utils";

interface ContactCardProps {
icon: React.ReactNode;
title: string;
description: string;
links: { label: string; url: string }[];
direction?: "row" | "col";
}

export const ContactCard = ({
icon,
title,
description,
links,
direction = "row",
}: ContactCardProps) => {
const directionClass = cn({
"flex-row": direction === "row",
"flex-col": direction === "col",
});

return (
<div className="border-gray-250 flex h-[15rem] flex-col justify-between rounded-xl border bg-background p-4">
<div className="border-gray-250 flex h-[36px] w-[36px] items-center justify-center rounded-lg border">
{icon}
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<p className="text-lg font-medium">{title}</p>
<p className="text-base text-black/80">{description}</p>
</div>
<div className={cn("flex flex-wrap gap-2", directionClass)}>
{links.map(({ label, url }) => (
<Link
key={label}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium underline"
>
{label}
</Link>
))}
</div>
</div>
</div>
);
};
128 changes: 100 additions & 28 deletions components/header-section.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,121 @@
"use client";

import { useTranslations } from "next-intl";
import { motion } from "motion/react";
import { useInView } from "react-intersection-observer";
import { cn } from "@/lib/utils";

interface HeaderSelectionProps {
section: string;
section?: string;
badge?: string;
title?: string;
description?: string;
badgeAlignment?: "left" | "center" | "right";
titleAlignment?: "left" | "center" | "right";
descriptionAlignment?: "left" | "center" | "right";
maxWidth?: string | number;
}

export default function HeaderSection({ section }: HeaderSelectionProps) {
export default function HeaderSection({
section,
badge,
title,
description,
badgeAlignment = "center",
titleAlignment = "center",
descriptionAlignment = "center",
maxWidth = 540,
}: HeaderSelectionProps) {
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: true,
});

const t = useTranslations(section);
const t = useTranslations(section || "");

const getTranslation = (key: string) => {
if (t) {
try {
return t(key);
} catch {
console.warn(
`Translation key "${key}" not found in section "${section}"`
);
return null;
}
}
return null;
};

const badgeContent = getTranslation("badge") || badge;
const titleContent = getTranslation("title") || title;
const descriptionContent = getTranslation("description") || description;

const badgeJustifyClass = cn({
"justify-start": badgeAlignment === "left",
"justify-center": badgeAlignment === "center",
"justify-end": badgeAlignment === "right",
});

const titleTextAlignClass = cn({
"text-left": titleAlignment === "left",
"text-center": titleAlignment === "center",
"text-right": titleAlignment === "right",
});

const descriptionTextAlignClass = cn({
"text-left": descriptionAlignment === "left",
"text-center": descriptionAlignment === "center",
"text-right": descriptionAlignment === "right",
});

const maxWidthClass =
typeof maxWidth === "number"
? `max-w-[${maxWidth}px]`
: `max-w-[${maxWidth}]`;

return (
<>
<div className="mx-auto max-w-[540px]" ref={ref}>
<div className="flex justify-center">
<motion.div
<div className={cn("mx-auto", maxWidthClass)} ref={ref}>
{badgeContent && (
<div className={cn("flex", badgeJustifyClass)}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 }}
className="flex h-9 items-center rounded-md border border-input bg-background px-3 text-sm font-medium hover:bg-accent"
>
{badgeContent}
</motion.div>
</div>
)}

{titleContent && (
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.4 }}
className={cn(
"mt-5 text-4xl font-bold tracking-tighter md:text-[54px] md:leading-[60px]",
titleTextAlignClass
)}
dangerouslySetInnerHTML={{ __html: titleContent }}
/>
)}

{descriptionContent && (
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 }}
className="flex h-9 items-center rounded-md border border-input bg-background px-3 text-center text-sm font-medium hover:bg-accent"
transition={{ duration: 0.5, delay: 0.6 }}
className={cn(
"mt-5 text-xl tracking-tighter text-black/60 md:text-[22px] md:leading-[30px]",
descriptionTextAlignClass
)}
>
{t("badge")}
</motion.div>
</div>

<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.4 }}
className="mt-5 text-center text-4xl font-bold tracking-tighter md:text-[54px] md:leading-[60px]"
dangerouslySetInnerHTML={{ __html: t("title") }}
/>

<motion.p
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.6 }}
className="mt-5 text-center text-xl tracking-tighter text-black/60 md:text-[22px] md:leading-[30px]"
>
{t("description")}
</motion.p>
{descriptionContent}
</motion.p>
)}
</div>
</>
);
Expand Down
Loading

0 comments on commit 5d2f89a

Please sign in to comment.