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

WEB-9: Active announcements #22

Merged
merged 4 commits into from
Aug 20, 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
Binary file added public/app-icons/CourseGrab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/app-icons/Eatery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/app-icons/Resell.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/app-icons/Scooped.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/app-icons/Transit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/app-icons/Uplift.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/app-icons/Volume.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 38 additions & 19 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import UpcomingAnnouncements from "@/components/landing/UpcomingAnnouncements";
import ActiveAnnouncements from "@/components/landing/ActiveAnnouncements";
import ActiveCell from "@/components/landing/ActiveCell";
import { Announcement } from "@/models/Announcement";
import { AppName } from "@/models/AppName";

Expand All @@ -16,19 +17,39 @@ const pastAnn: Announcement = {
title: "Demo Day",
};

const ann1: Announcement = {
const liveAnn: Announcement = {
id: "65f3c6c85ec12921d8bbd0e3",
apps: [AppName.UPLIFT],
body: "Short body.",
endDate: new Date("2024-08-16T03:00:00Z"),
endDate: new Date("2024-08-25T19:15:00Z"),
imageUrl:
"https://kidszoo.org/wp-content/uploads/2017/06/IMG_2034-2-scaled.jpg",
"https://runningscaredsite.wordpress.com/wp-content/uploads/2016/01/running-scared-chicken.jpeg?w=640",
link: "https://www.instagram.com/p/C4ft4SyOaUj/",
startDate: new Date("2024-08-10T23:42:20Z"),
startDate: new Date("2024-07-10T23:42:20Z"),
title: "Title",
};

const ann2: Announcement = {
const liveAnn2: Announcement = {
id: "65f3c6c85ec12921d8bbd0e3",
apps: [
AppName.TRANSIT,
AppName.EATERY,
AppName.SCOOPED,
AppName.COURSEGRAB,
AppName.RESELL,
AppName.UPLIFT,
AppName.VOLUME,
],
body: "Come ASAP.",
endDate: new Date("2024-08-28T19:15:00Z"),
imageUrl:
"https://cdn.britannica.com/55/174255-050-526314B6/brown-Guernsey-cow.jpg",
link: "https://www.instagram.com/p/C4ft4SyOaUj/",
startDate: new Date("2024-08-01T23:42:20Z"),
title: "Happening now.",
};

const futureAnn: Announcement = {
id: "65f3c6c85ec12921d8bbd0e3",
apps: [AppName.EATERY, AppName.RESELL, AppName.UPLIFT, AppName.TRANSIT],
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
Expand All @@ -40,31 +61,29 @@ const ann2: Announcement = {
title: "Very, Very, Very Big Announcement.",
};

const ann3: Announcement = {
const futureAnn2: Announcement = {
id: "65f3c6c85ec12921d8bbd0e3",
apps: [AppName.RESELL],
body: "Starting at the same time as ...",
endDate: new Date("2024-08-16T03:00:00Z"),
endDate: new Date("2025-08-09T18:57:00Z"),
imageUrl:
"https://kidszoo.org/wp-content/uploads/2017/06/IMG_2034-2-scaled.jpg",
link: "https://www.instagram.com/p/C4ft4SyOaUj/",
startDate: new Date("2024-08-10T23:42:20Z"),
startDate: new Date("2025-08-08T19:24:20Z"),
title: "Announcement",
};

export default function Landing() {
return (
<div className="w-[361px] md:w-[770px] lg:w-[499px]">
<div className="w-[361px] md:w-[770px] lg:w-[597px]">
{/* empty state (there are no announcements) */}
<UpcomingAnnouncements announcements={[]} />
{/* empty state (there are no upcoming announcements, only past announcements) */}
<UpcomingAnnouncements announcements={[pastAnn]} />
{/* only one announcement, which is upcoming */}
<UpcomingAnnouncements announcements={[ann2]} />
{/* multiple upcoming announcements with different start times, and one past announcement */}
<UpcomingAnnouncements announcements={[pastAnn, ann1, ann2]} />
{/* multiple upcoming announcements with the same start time, and one past announcemnet */}
<UpcomingAnnouncements announcements={[pastAnn, ann1, ann3]} />
<ActiveAnnouncements announcements={[]} />
{/* empty state (there are only past announcements) */}
<ActiveAnnouncements announcements={[pastAnn]} />
{/* multiple announcements, including ones that are currently live and ones that are in the future */}
<ActiveAnnouncements
announcements={[pastAnn, liveAnn, liveAnn2, futureAnn, futureAnn2]}
/>
</div>
);
}
72 changes: 72 additions & 0 deletions src/components/landing/ActiveAnnouncements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import CalendarArrowIcon from "@/icons/CalendarArrowIcon";
import { Announcement } from "@/models/Announcement";
import { NO_ANNOUNCEMENTS_MESSAGE } from "@/utils/constants";
import {
calculateTimeRemaining,
filterActiveAnnouncements,
sortAnnouncementsByStartDate,
} from "@/utils/utils";
import ActiveCell from "./ActiveCell";
import { useState, useEffect } from "react";

interface Props {
announcements: Announcement[];
}

export default function ActiveAnnouncements({ announcements }: Props) {
const activeAnnouncements = sortAnnouncementsByStartDate(
filterActiveAnnouncements(announcements)
);

const [timeRemaining, setTimeRemaining] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});

useEffect(() => {
if (activeAnnouncements.length === 0) return;

const firstAnnouncement = activeAnnouncements[0];
const startDate = new Date(firstAnnouncement.startDate);

const updateCountdown = () => {
setTimeRemaining(calculateTimeRemaining(startDate));
};

updateCountdown();
const interval = setInterval(updateCountdown, 1000);

return () => clearInterval(interval);
}, [announcements]);

return (
<div className="flex flex-col p-6 items-start gap-6 rounded-lg bg-neutral-white">
<div className="flex items-center gap-4 self-stretch">
<CalendarArrowIcon className="w-[32px] md:w-[40px] h-[32px] md:h-[40px] fill-neutral-800"></CalendarArrowIcon>
<div className="flex flex-col">
<h4 className="self-stretch text-neutral-800">
Active Announcements
</h4>
<p className="b1 self-stretch text-neutral-600">
Current and upcoming announcements.
</p>
</div>
</div>
{activeAnnouncements.length > 0 ? (
<div className="flex flex-col items-start self-stretch bg-neutral-white rounded-lg gap-3">
{activeAnnouncements.map((announcement) => (
<ActiveCell key={announcement.id} announcement={announcement} />
))}
</div>
) : (
<p className="b1 self-stretch text-neutral-400 text-center">
{NO_ANNOUNCEMENTS_MESSAGE}
</p>
)}
</div>
);
}
70 changes: 70 additions & 0 deletions src/components/landing/ActiveCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import AppIcon from "@/icons/AppIcon";
import EditIcon from "@/icons/EditIcon";
import TertiaryButton from "../shared/TertiaryButton";
import { Announcement } from "@/models/Announcement";
import { dateInRange, formatDate } from "@/utils/utils";
import { useEffect, useState } from "react";
import LiveIndicator from "../shared/LiveIndicator";

interface Props {
announcement: Announcement;
}

export default function ActiveCell({ announcement }: Props) {
const [currentDate, setCurrentDate] = useState(new Date());

useEffect(() => {
const intervalId = setInterval(() => {
setCurrentDate(new Date());
}, 500);

return () => clearInterval(intervalId);
}, []);

return (
<div className="flex flex-col p-6 items-start md:items-end md:flex-row justify-center gap-6 md:gap-8 self-stretch bg-neutral-white rounded-lg border border-other-stroke relative">
<img
src={announcement.imageUrl}
className="h-[265px] md:w-[108px] md:h-[108px] self-stretch rounded-lg object-cover bg-center"
/>
<div className="flex flex-col items-start gap-4 self-stretch md:justify-between md:w-full">
<div className="flex items-start gap-6 self-stretch w-full md:justify-between">
<div className="flex flex-col gap-1">
<h4 className="self-stretch text-neutral-800">
{announcement.title}
</h4>
<p className="b1 self-stretch text-neutral-600">
{" "}
{formatDate(announcement.startDate)} -{" "}
{formatDate(announcement.endDate)}{" "}
</p>
</div>
<TertiaryButton
text="Edit"
action={() => console.log("Button clicked")}
className="max-md:hidden"
/>
</div>
<div className="flex h-[32px] items-center gap-2">
{announcement.apps.map((app) => (
<AppIcon appName={app} className="rounded-sm w-[32px] h-[32px]" />
))}
</div>
<TertiaryButton
text="Edit"
action={() => console.log("Button clicked")}
className="md:hidden"
/>
</div>
{dateInRange(
currentDate,
announcement.startDate,
announcement.endDate
) ? (
<LiveIndicator />
) : null}
</div>
);
}
8 changes: 8 additions & 0 deletions src/components/shared/LiveIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function LiveIndicator() {
return (
<div className="flex h-[32px] py-2 px-3 items-center gap-1 absolute right-[40px] top-[40px] md:bottom-[24px] md:right-[24px] md:top-auto bg-green-100 rounded-xl">
<div className="w-[10px] h-[10px] bg-green-600 border-2 border-green-300 rounded-xl" />
<div className="label text-green-600 text-center">LIVE</div>
</div>
);
}
21 changes: 21 additions & 0 deletions src/components/shared/TertiaryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import EditIcon from "@/icons/EditIcon";

interface Props {
text: string;
action: () => void;
className?: string
}

export default function TertiaryButton({ text, action, className }: Props) {
return (
<button
className={`flex py-2 px-4 justify-center items-center gap-2 rounded-xl border border-other-stroke bg-neutral-white ${className}`}
onClick={action}
>
<EditIcon className="w-[16px] h-[16px] stroke-neutral-black"></EditIcon>
<p className="b1 text-neutral-800 text-center">{text}</p>
</button>
);
}
31 changes: 31 additions & 0 deletions src/icons/AppIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AppName } from "@/models/AppName";
import { IconProps } from "@/models/IconProps";

interface Props extends IconProps {
appName: AppName;
}

export default function AppIcon({ appName, className }: Props) {
const getAppSrc = (appName: AppName): string => {
switch (appName) {
case AppName.TRANSIT:
return "/app-icons/Transit.png";
case AppName.EATERY:
return "/app-icons/Eatery.png";
case AppName.RESELL:
return "/app-icons/Resell.png";
case AppName.COURSEGRAB:
return "/app-icons/CourseGrab.png";
case AppName.VOLUME:
return "/app-icons/Volume.png";
case AppName.UPLIFT:
return "/app-icons/Uplift.png";
case AppName.SCOOPED:
return "/app-icons/Scooped.png";
default:
throw new Error(`No icon found for app name: ${appName}`);
}
};

return <img src={getAppSrc(appName)} className={className} />;
}
15 changes: 15 additions & 0 deletions src/icons/CalendarArrowIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IconProps } from "@/models/IconProps";

export default function ({ className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 25 24"
className={className}
>
<path
d="M15.5 22V20H19.5V10H5.5V14H3.5V6C3.5 5.45 3.696 4.97933 4.088 4.588C4.48 4.19667 4.95067 4.00067 5.5 4H6.5V2H8.5V4H16.5V2H18.5V4H19.5C20.05 4 20.521 4.196 20.913 4.588C21.305 4.98 21.5007 5.45067 21.5 6V20C21.5 20.55 21.3043 21.021 20.913 21.413C20.5217 21.805 20.0507 22.0007 19.5 22H15.5ZM8.5 24L7.1 22.6L9.675 20H1.5V18H9.675L7.1 15.4L8.5 14L13.5 19L8.5 24ZM5.5 8H19.5V6H5.5V8Z"
/>
</svg>
);
}
26 changes: 26 additions & 0 deletions src/icons/EditIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IconProps } from "@/models/IconProps";

export default function ({ className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
className={className}
>
<path
d="M8 2H3.33333C2.97971 2 2.64057 2.14048 2.39052 2.39052C2.14048 2.64057 2 2.97971 2 3.33333V12.6667C2 13.0203 2.14048 13.3594 2.39052 13.6095C2.64057 13.8595 2.97971 14 3.33333 14H12.6667C13.0203 14 13.3594 13.8595 13.6095 13.6095C13.8595 13.3594 14 13.0203 14 12.6667V8"
fill="none"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12.2502 1.75003C12.5154 1.48481 12.8751 1.33582 13.2502 1.33582C13.6252 1.33582 13.9849 1.48481 14.2502 1.75003C14.5154 2.01525 14.6644 2.37496 14.6644 2.75003C14.6644 3.1251 14.5154 3.48481 14.2502 3.75003L8.00016 10L5.3335 10.6667L6.00016 8.00003L12.2502 1.75003Z"
fill="none"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
1 change: 1 addition & 0 deletions src/models/AppName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export enum AppName {
COURSEGRAB = "coursegrab",
VOLUME = "volume",
UPLIFT = "uplift",
SCOOPED = "scooped",
}
Loading
Loading