diff --git a/.gitignore b/.gitignore index 9b7c041..bf6eb31 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ # React Router /.react-router/ /build/ + +.env \ No newline at end of file diff --git a/README.md b/README.md index e6afc4f..2b0ea77 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,22 @@ -

- CSSG Logo -

+# CCC -# CSSG Starter Template +A CS + SG project in partnership with The Campus & Community Coalition -This is a starter template for CSSG projects using React, TypeScript, and Tailwind CSS. It is configured to be used with VS Code Dev Containers for a consistent development environment. +## About The Campus & Community Coalition -## Prerequisites +The Campus & Community Coalition (CCC) is a collaborative force bringing together university and community partners to address the harms associated with high-risk drinking. By fostering open dialogue, sharing power, and using data-driven strategies, they work to create an environment where everyone can thrive socially, academically, and economically. -Before you begin, ensure you have the following installed: +## Project Mission -- [Git](https://git-scm.com/) -- [Docker Desktop](https://www.docker.com/products/docker-desktop) -- [Visual Studio Code](https://code.visualstudio.com/) -- [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code. +CCC wants to migrate from under Downtown Chapel Hill's website to their own. [Click here to see the current website.](https://www.downtownchapelhill.com/coalition) -## Getting Started +## Project Overview -1. **Fork the repository:** +This website will be used primarily to display important trends regarding alcohol use in the Chapel Hill community alongside other important resources. - Fork this repository. Then, clone your forked repository: +This webpage uses React. [Click here to get started with React.](https://react.dev/learn) - ```bash - git clone - cd - ``` +## Get Started -2. **Open in VS Code/Cursor:** - - Open the cloned repository folder in VS Code or Cursor. - -3. **Open in Dev Container:** - - Once the project is open in VS Code, you will be prompted to "Reopen in Container". Click on it. - - If you don't see the prompt, you can open the command palette and run "Dev Containers: Reopen in Container". - - **Windows/Linux:** `Ctrl+Shift+P` - - **Mac:** `Cmd+Shift+P` - - This will build the Docker container for the development environment. The first build might take a few minutes. Subsequent loads will be much faster. - -## Available Commands - -Inside the dev container, you can use the following commands: - -| Command | Description | -| :------------------ | :--------------------------------------------------------- | -| `npm run dev` | Starts the development server with Hot Module Replacement. | -| `npm run build` | Builds the application for production. | -| `npm run start` | Serves the production build. | -| `npm run lint` | Lints the codebase using ESLint. | -| `npm run lint:fix` | Lints and automatically fixes issues. | -| `npm run format` | Formats the code using Prettier. | -| `npm run typecheck` | Runs the TypeScript compiler to check for type errors. | +Look at our environment setup docs: [Environment Setup](docs/environment_setup.md)\ +Also, make sure to look over the: [Contributing Guidelines](docs/contributing_guidelines.md) diff --git a/app/app.css b/app/app.css index 242d4f3..ac8ad18 100644 --- a/app/app.css +++ b/app/app.css @@ -1,14 +1,13 @@ @import "tailwindcss"; @theme { - --font-sans: - "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans: "Figtree"; } html, body { - @apply bg-white dark:bg-gray-950; + @apply bg-white text-black; + color-scheme: light; @media (prefers-color-scheme: dark) { color-scheme: dark; diff --git a/app/assets/CCC-Strategic-Plan-2024-27-1.pdf b/app/assets/CCC-Strategic-Plan-2024-27-1.pdf new file mode 100644 index 0000000..7eccd23 Binary files /dev/null and b/app/assets/CCC-Strategic-Plan-2024-27-1.pdf differ diff --git a/app/assets/CCC-The-Burden-of-Excessive-Drinking-in-Orange-County-NC-Final-Report.Report.4.pdf b/app/assets/CCC-The-Burden-of-Excessive-Drinking-in-Orange-County-NC-Final-Report.Report.4.pdf new file mode 100644 index 0000000..d371d1c Binary files /dev/null and b/app/assets/CCC-The-Burden-of-Excessive-Drinking-in-Orange-County-NC-Final-Report.Report.4.pdf differ diff --git a/app/assets/CCC-UNC-SWCCC-CoD-Final-Report.Report.3.pdf b/app/assets/CCC-UNC-SWCCC-CoD-Final-Report.Report.3.pdf new file mode 100644 index 0000000..5215426 Binary files /dev/null and b/app/assets/CCC-UNC-SWCCC-CoD-Final-Report.Report.3.pdf differ diff --git a/app/assets/Community-level-consequences-of-high-risk-drinking_final.Report.2.pdf b/app/assets/Community-level-consequences-of-high-risk-drinking_final.Report.2.pdf new file mode 100644 index 0000000..0389c28 Binary files /dev/null and b/app/assets/Community-level-consequences-of-high-risk-drinking_final.Report.2.pdf differ diff --git a/app/assets/Town-Gown-Full-Report_final.Report.1.pdf b/app/assets/Town-Gown-Full-Report_final.Report.1.pdf new file mode 100644 index 0000000..0a447a4 Binary files /dev/null and b/app/assets/Town-Gown-Full-Report_final.Report.1.pdf differ diff --git a/app/assets/facebook-logo.png b/app/assets/facebook-logo.png new file mode 100644 index 0000000..a50d06f Binary files /dev/null and b/app/assets/facebook-logo.png differ diff --git a/app/assets/icons/additional-partners.svg b/app/assets/icons/additional-partners.svg new file mode 100644 index 0000000..5d1719f --- /dev/null +++ b/app/assets/icons/additional-partners.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/icons/location.svg b/app/assets/icons/location.svg new file mode 100644 index 0000000..70c7eea --- /dev/null +++ b/app/assets/icons/location.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/mail.svg b/app/assets/icons/mail.svg new file mode 100644 index 0000000..a979a34 --- /dev/null +++ b/app/assets/icons/mail.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/orange-county.svg b/app/assets/icons/orange-county.svg new file mode 100644 index 0000000..e9dcbed --- /dev/null +++ b/app/assets/icons/orange-county.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/icons/our-mission.svg b/app/assets/icons/our-mission.svg new file mode 100644 index 0000000..56b65f2 --- /dev/null +++ b/app/assets/icons/our-mission.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/icons/our-values.svg b/app/assets/icons/our-values.svg new file mode 100644 index 0000000..8d599d2 --- /dev/null +++ b/app/assets/icons/our-values.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/icons/reports.svg b/app/assets/icons/reports.svg new file mode 100644 index 0000000..0b0d2a1 --- /dev/null +++ b/app/assets/icons/reports.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/icons/strategic-plan.svg b/app/assets/icons/strategic-plan.svg new file mode 100644 index 0000000..c7d4c16 --- /dev/null +++ b/app/assets/icons/strategic-plan.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/town-of-chapel-hill.svg b/app/assets/icons/town-of-chapel-hill.svg new file mode 100644 index 0000000..748591a --- /dev/null +++ b/app/assets/icons/town-of-chapel-hill.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/unc-chapel-hill.svg b/app/assets/icons/unc-chapel-hill.svg new file mode 100644 index 0000000..24b7a10 --- /dev/null +++ b/app/assets/icons/unc-chapel-hill.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/instagram-logo.png b/app/assets/instagram-logo.png new file mode 100644 index 0000000..7818a57 Binary files /dev/null and b/app/assets/instagram-logo.png differ diff --git a/app/assets/logo.png b/app/assets/logo.png new file mode 100644 index 0000000..46a4b1d Binary files /dev/null and b/app/assets/logo.png differ diff --git a/app/assets/newsletter-left.jpg b/app/assets/newsletter-left.jpg new file mode 100644 index 0000000..b3a66b5 Binary files /dev/null and b/app/assets/newsletter-left.jpg differ diff --git a/app/assets/newsletter-right.png b/app/assets/newsletter-right.png new file mode 100644 index 0000000..21dd41b Binary files /dev/null and b/app/assets/newsletter-right.png differ diff --git a/app/components/archivedNewsletterCard.tsx b/app/components/archivedNewsletterCard.tsx new file mode 100644 index 0000000..506c4b3 --- /dev/null +++ b/app/components/archivedNewsletterCard.tsx @@ -0,0 +1,65 @@ +type ArchivedNewsletterCardProps = { + title: string; + date: string; + href?: string; +}; + +export default function ArchivedNewsletterCard({ + title, + date, + href, +}: ArchivedNewsletterCardProps) { + const ArrowIcon = ( + + + + ); + + return ( +
+
+

{title}

+

{date}

+
+
+ {href ? ( + + View + + + ) : ( + + View + + + )} +
+
+ ); +} diff --git a/app/components/footer.tsx b/app/components/footer.tsx new file mode 100644 index 0000000..8f3ee11 --- /dev/null +++ b/app/components/footer.tsx @@ -0,0 +1,161 @@ +import { Link } from "react-router"; + +export default function Footer() { + const footerStyle = { + width: "100%", + backgroundColor: "lightgrey", + padding: "60px 80px", + display: "flex", + justifyContent: "space-between", + alignItems: "flex-start", + boxSizing: "border-box", + } as const; + + const leftSection = { + display: "flex", + flexDirection: "column", + gap: "12px", + color: "black", + fontSize: "0.95rem", + lineHeight: "1.6", + marginTop: "40px", + } as const; + + const headingStyle = { + fontSize: "1.5rem", + fontWeight: "500", + marginBottom: "8px", + } as const; + + const rightSection = { + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + gap: "32px", + } as const; + + const logoStyle = { + width: "140px", + height: "auto", + objectFit: "contain", + } as const; + + const iconRowStyle = { + display: "flex", + flexDirection: "row", + gap: "20px", + alignItems: "center", + } as const; + + const circleStyle = { + width: "48px", + height: "48px", + borderRadius: "50%", + display: "flex", + justifyContent: "center", + alignItems: "center", + overflow: "hidden", + } as const; + + const iconStyle = { + width: "100%", + height: "100%", + objectFit: "cover", + borderRadius: "50%", + } as const; + + const navStyle = { + display: "flex", + gap: "40px", + color: "black", + fontSize: "0.95rem", + textTransform: "lowercase", + } as const; + + const linkStyle = { + color: "black", + textDecoration: "none", + opacity: 0.9, + } as const; + + return ( + + ); +} diff --git a/app/components/header.tsx b/app/components/header.tsx new file mode 100644 index 0000000..3b9f1bc --- /dev/null +++ b/app/components/header.tsx @@ -0,0 +1,225 @@ +import { useEffect, useState } from "react"; +import logo from "app/assets/logo.png"; +import { Link } from "react-router-dom"; + +const BREAKPOINT = 870; + +function Header() { + const [shrink, setShrink] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setShrink(window.scrollY > 100); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + // Handing window resize to toggle mobile view + useEffect(() => { + setIsMobile(window.innerWidth < BREAKPOINT); + const handleResize = () => { + const newIsMobile = window.innerWidth < BREAKPOINT; + setIsMobile(newIsMobile); + if (!newIsMobile) { + setIsMenuOpen(false); + } + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + const headerStyle = { + position: "sticky", + top: shrink ? "0px" : "40px", + margin: "0 auto", + marginTop: "40px", + marginBottom: "10px", + marginLeft: shrink ? "0px" : "40px", + marginRight: shrink ? "0px" : "40px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + backgroundColor: "lightgray", + borderRadius: shrink ? "0px" : "100px", + padding: shrink ? "5px 20px" : "0px 20px", + fontSize: shrink ? "18px" : "20px", + flexWrap: "nowrap", + gap: "125px", + zIndex: 1000, + transition: "all 0.4s ease", + } as const; + + const linkContainerStyle = { + display: isMobile ? "none" : "flex", + justifyContent: "center", + alignItems: "center", + gap: "80px", + flex: "1", + textAlign: "center", + } as const; + + const hamburgerStyle = { + display: "flex", + flexDirection: "column", + justifyContent: "space-around", + width: "30px", + height: "30px", + cursor: "pointer", + padding: "5px", + } as const; + + const lineStyle = { + width: "100%", + height: "3px", + backgroundColor: "black", + borderRadius: "10px", + } as const; + + const textStyle = { + textAlign: "center", + flexShrink: 5, + objectFit: "contain", + } as const; + + const fullscreenMenuStyle = { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "white", + display: isMobile ? "flex" : "none", + flexDirection: "column", + alignItems: "center", + transform: isMenuOpen ? "translateY(0)" : "translateY(-100%)", + transition: "transform 0.4s ease-in-out", + zIndex: 10000, + paddingTop: "60px", + overflowY: "auto", + } as const; + + const menuLinkStyle = { + padding: "20px 0", + fontSize: "24px", + fontWeight: "bold", + color: "black", + textDecoration: "none", + width: "80%", + textAlign: "center", + borderBottom: "1px solid #ccc", + } as const; + + // Close button (X) style for the drawer + const closeButtonStyle = { + position: "absolute", + top: "20px", + right: "40px", + fontSize: "36px", + cursor: "pointer", + fontWeight: "lighter", + color: "black", + zIndex: 10001, + } as const; + + return ( + <> +
+
+ + Logo + + +

+ Campus & Community Coalition +

+ +
+
+ + Who We Are + + + Research + + + Data + + + Our Newsletter + +
+ + {/*Hamburger Icon */} + {isMobile && ( + + )} +
+ + {/* Fullscreen Drawer Menu for Mobile */} +
+
+ × +
+ + Home + + + Who We Are + + + Research + + + Data + + + Our Newsletter + +
+ + ); +} + +export default Header; diff --git a/app/components/planCard.tsx b/app/components/planCard.tsx new file mode 100644 index 0000000..74c53ec --- /dev/null +++ b/app/components/planCard.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +interface PlanCardProps { + title?: string; + summary?: string; + buttonText?: string; + onClick?: () => void; +} + +const BLUE_COLOR = "text-[#499ED7]"; +const BORDER_COLOR = "border-[#499ED7]"; + +export default function PlanCard({ + title = "Placeholder Plan Title", + summary = "Placeholder description for the plan card component.", +}: PlanCardProps) { + return ( +
+
+

+ {title} +

+

+ {summary} +

+
+
+ ); +} diff --git a/app/components/reportsCard.tsx b/app/components/reportsCard.tsx new file mode 100644 index 0000000..4c1b892 --- /dev/null +++ b/app/components/reportsCard.tsx @@ -0,0 +1,58 @@ +import React from "react"; + +interface ReportCardProps { + title?: string; + summary?: string; + buttonText?: string; + onClick?: () => void; +} + +export default function ReportCard({ + title = "Report Title", + summary = "Placeholder description for the report card.", + buttonText = "View Report", + onClick, +}: ReportCardProps) { + return ( +
+
+

+ {title} +

+

{summary}

+
+ +
+ +
+
+ ); +} diff --git a/app/helpers/validation.ts b/app/helpers/validation.ts new file mode 100644 index 0000000..dd13d23 --- /dev/null +++ b/app/helpers/validation.ts @@ -0,0 +1,20 @@ +const DISALLOWED_EMAIL_CHARS = /[\s()[\];:<>\\/"'`~!#$%^&*|+=?{}]/; +const BASIC_EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; + +export function normalizeEmail(value: string): string { + return (value || "").trim().toLowerCase(); +} + +export function isEmailValid(value: string): boolean { + const email = normalizeEmail(value); + + if (!email) return false; + if (DISALLOWED_EMAIL_CHARS.test(email)) return false; + if (!BASIC_EMAIL_SHAPE.test(email)) return false; + + return true; +} + +export function areRequiredFilled(obj: Record): boolean { + return Object.values(obj).every((v) => (v || "").trim().length > 0); +} diff --git a/app/root.tsx b/app/root.tsx index e35a017..3ce97b8 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -9,6 +9,7 @@ import { import type { Route } from "./+types/root"; import "./app.css"; +import Footer from "./components/footer"; export const links: Route.LinksFunction = () => [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, @@ -19,7 +20,7 @@ export const links: Route.LinksFunction = () => [ }, { rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + href: "https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap", }, ]; @@ -39,13 +40,18 @@ export function Layout({ children }: { children: React.ReactNode }) { {children} +