diff --git a/frontend/.env.example b/frontend/.env.example index 3304aa4d..a6412939 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,4 @@ -NEXT_PUBLIC_APP_NAME=TODO +NEXT_PUBLIC_APP_NAME=Jippy NEXT_PUBLIC_FRONTEND_URL="http://localhost:3000" NEXT_PUBLIC_BACKEND_URL="http://localhost:8000" NEXT_PUBLIC_GOOGLE_CLIENT_ID= \ No newline at end of file diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico index 718d6fea..d86998f4 100644 Binary files a/frontend/app/favicon.ico and b/frontend/app/favicon.ico differ diff --git a/frontend/app/home.tsx b/frontend/app/home.tsx new file mode 100644 index 00000000..3a77e4db --- /dev/null +++ b/frontend/app/home.tsx @@ -0,0 +1,26 @@ +import NewsArticle from "@/components/news/news-article"; + +/* This component should only be rendered to authenticated users */ +const Home = () => { + return ( +
+
+ + {new Date().toDateString()} + +

+ What happened this week +

+
+ +
+ + + + +
+
+ ); +}; + +export default Home; diff --git a/frontend/app/landing.tsx b/frontend/app/landing.tsx new file mode 100644 index 00000000..240621a1 --- /dev/null +++ b/frontend/app/landing.tsx @@ -0,0 +1,6 @@ +const Landing = () => { + // TODO: build landing page + return <>; +}; + +export default Landing; diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index d7f81357..6c748be1 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -23,6 +23,7 @@ import { CardTitle, } from "@/components/ui/card"; import { Form } from "@/components/ui/form"; +import { useUserStore } from "@/store/user/user-store-provider"; const loginFormSchema = z.object({ email: z.string().email("Invalid email address"), @@ -38,7 +39,10 @@ type LoginForm = z.infer; function LoginPage() { const router = useRouter(); + const isLoggedIn = useUserStore((state) => state.isLoggedIn); + const setLoggedIn = useUserStore((state) => state.setLoggedIn); const [isError, setIsError] = useState(false); + const form = useForm({ resolver: zodResolver(loginFormSchema), defaultValues: loginFormDefault, @@ -54,10 +58,15 @@ function LoginPage() { setIsError(true); } else { setIsError(false); + setLoggedIn(response.data.user); router.push("/"); } }; + if (isLoggedIn) { + router.push("/"); + } + return ( diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx new file mode 100644 index 00000000..5772572b --- /dev/null +++ b/frontend/app/not-found.tsx @@ -0,0 +1,32 @@ +import JippyClown from "@/assets/jippy-clown"; +import Link from "@/components/navigation/link"; +import { Button } from "@/components/ui/button"; + +function AboutPage() { + return ( +
+
+ +
+

+ 404 Not Found Error +

+

+ Ribbit! Jippy couldn't find that page... +

+
+
+
+

+ The page you were looking for doesn't exist. The page might have + moved, or you might have clicked on a bad link. +

+ + + +
+
+ ); +} + +export default AboutPage; diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 0df827d2..e983df1e 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,102 +1,12 @@ -import Image from "next/image"; +"use client"; -export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
+import Home from "@/app/home"; +import Landing from "@/app/landing"; +import { useUserStore } from "@/store/user/user-store-provider"; - -
+const RootPage = () => { + const isLoggedIn = useUserStore((store) => store.isLoggedIn); + return isLoggedIn ? : ; +}; - -
- ); -} +export default RootPage; diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx index 86c97c06..9b6c3b84 100644 --- a/frontend/app/register/page.tsx +++ b/frontend/app/register/page.tsx @@ -16,6 +16,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Box } from "@/components/ui/box"; import { Button } from "@/components/ui/button"; import { Form } from "@/components/ui/form"; +import { useUserStore } from "@/store/user/user-store-provider"; const registerFormSchema = z.object({ email: z.string().email("Invalid email address"), @@ -31,6 +32,7 @@ type RegisterForm = z.infer; function RegisterPage() { const router = useRouter(); + const isLoggedIn = useUserStore((state) => state.isLoggedIn); const [isError, setIsError] = useState(false); const form = useForm({ resolver: zodResolver(registerFormSchema), @@ -51,8 +53,12 @@ function RegisterPage() { } }; + if (isLoggedIn) { + router.push("/"); + } + return ( - + {/* Header */} diff --git a/frontend/assets/jippy-clown.tsx b/frontend/assets/jippy-clown.tsx new file mode 100644 index 00000000..5e00c423 --- /dev/null +++ b/frontend/assets/jippy-clown.tsx @@ -0,0 +1,41 @@ +const JippyClown = () => { + return ( + + + + + + + + + + + + + + + + ); +}; + +export default JippyClown; diff --git a/frontend/assets/jippy-icon/jippy-icon-lg.tsx b/frontend/assets/jippy-icon/jippy-icon-lg.tsx new file mode 100644 index 00000000..2ef9bece --- /dev/null +++ b/frontend/assets/jippy-icon/jippy-icon-lg.tsx @@ -0,0 +1,33 @@ +const JippyIconLg = ({ classname }: { classname?: string }) => { + return ( + + + + + + + ); +}; + +export default JippyIconLg; diff --git a/frontend/assets/jippy-icon/jippy-icon-md.tsx b/frontend/assets/jippy-icon/jippy-icon-md.tsx new file mode 100644 index 00000000..cb70cfee --- /dev/null +++ b/frontend/assets/jippy-icon/jippy-icon-md.tsx @@ -0,0 +1,33 @@ +const JippyIconMd = ({ classname }: { classname?: string }) => { + return ( + + + + + + + ); +}; + +export default JippyIconMd; diff --git a/frontend/assets/jippy-icon/jippy-icon-sm.tsx b/frontend/assets/jippy-icon/jippy-icon-sm.tsx new file mode 100644 index 00000000..6adea00a --- /dev/null +++ b/frontend/assets/jippy-icon/jippy-icon-sm.tsx @@ -0,0 +1,33 @@ +const JippyIconSm = ({ classname }: { classname?: string }) => { + return ( + + + + + + + ); +}; + +export default JippyIconSm; diff --git a/frontend/assets/jippy-icon/jippy-icon-xl.tsx b/frontend/assets/jippy-icon/jippy-icon-xl.tsx new file mode 100644 index 00000000..7269069c --- /dev/null +++ b/frontend/assets/jippy-icon/jippy-icon-xl.tsx @@ -0,0 +1,33 @@ +const JippyIconXl = ({ classname }: { classname?: string }) => { + return ( + + + + + + + ); +}; + +export default JippyIconXl; diff --git a/frontend/assets/jippy-logo/jippy-logo-lg.tsx b/frontend/assets/jippy-logo/jippy-logo-lg.tsx new file mode 100644 index 00000000..9e9e01c7 --- /dev/null +++ b/frontend/assets/jippy-logo/jippy-logo-lg.tsx @@ -0,0 +1,37 @@ +const JippyLogoLg = ({ classname }: { classname?: string }) => { + return ( + + + + + + + + ); +}; + +export default JippyLogoLg; diff --git a/frontend/assets/jippy-logo/jippy-logo-md.tsx b/frontend/assets/jippy-logo/jippy-logo-md.tsx new file mode 100644 index 00000000..61c9af22 --- /dev/null +++ b/frontend/assets/jippy-logo/jippy-logo-md.tsx @@ -0,0 +1,37 @@ +const JippyLogoMd = ({ classname }: { classname?: string }) => { + return ( + + + + + + + + ); +}; + +export default JippyLogoMd; diff --git a/frontend/assets/jippy-logo/jippy-logo-sm.tsx b/frontend/assets/jippy-logo/jippy-logo-sm.tsx new file mode 100644 index 00000000..64c0e65d --- /dev/null +++ b/frontend/assets/jippy-logo/jippy-logo-sm.tsx @@ -0,0 +1,37 @@ +const JippyLogoSm = ({ classname }: { classname?: string }) => { + return ( + + + + + + + + ); +}; + +export default JippyLogoSm; diff --git a/frontend/assets/jippy-logo/jippy-logo-xl.tsx b/frontend/assets/jippy-logo/jippy-logo-xl.tsx new file mode 100644 index 00000000..0f934ca7 --- /dev/null +++ b/frontend/assets/jippy-logo/jippy-logo-xl.tsx @@ -0,0 +1,37 @@ +const JippyLogoXl = ({ classname }: { classname?: string }) => { + return ( + + + + + + + + ); +}; + +export default JippyLogoXl; diff --git a/frontend/client/core/utils.ts b/frontend/client/core/utils.ts index 309135e0..f7103fc4 100644 --- a/frontend/client/core/utils.ts +++ b/frontend/client/core/utils.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Config } from "./types"; interface PathSerializer { diff --git a/frontend/client/schemas.gen.ts b/frontend/client/schemas.gen.ts index e8916813..a4999a29 100644 --- a/frontend/client/schemas.gen.ts +++ b/frontend/client/schemas.gen.ts @@ -1,5 +1,43 @@ // This file is auto-generated by @hey-api/openapi-ts +export const AnalysisDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + category: { + $ref: "#/components/schemas/CategoryDTO", + }, + content: { + type: "string", + title: "Content", + }, + }, + type: "object", + required: ["id", "category", "content"], + title: "AnalysisDTO", +} as const; + +export const AnswerDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + points: { + items: { + $ref: "#/components/schemas/PointMiniDTO", + }, + type: "array", + title: "Points", + }, + }, + type: "object", + required: ["id", "points"], + title: "AnswerDTO", +} as const; + export const Body_log_in_auth_login_postSchema = { properties: { grant_type: { @@ -55,6 +93,143 @@ export const Body_log_in_auth_login_postSchema = { title: "Body_log_in_auth_login_post", } as const; +export const CategoryDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["id", "name"], + title: "CategoryDTO", +} as const; + +export const CreateUserQuestionSchema = { + properties: { + question: { + type: "string", + title: "Question", + }, + }, + type: "object", + required: ["question"], + title: "CreateUserQuestion", +} as const; + +export const EventDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + title: { + type: "string", + title: "Title", + }, + description: { + type: "string", + title: "Description", + }, + is_singapore: { + type: "boolean", + title: "Is Singapore", + }, + date: { + type: "string", + format: "date-time", + title: "Date", + }, + categories: { + items: { + $ref: "#/components/schemas/CategoryDTO", + }, + type: "array", + title: "Categories", + }, + analysises: { + items: { + $ref: "#/components/schemas/AnalysisDTO", + }, + type: "array", + title: "Analysises", + }, + gp_questions: { + items: { + $ref: "#/components/schemas/GPQuestionDTO", + }, + type: "array", + title: "Gp Questions", + }, + }, + type: "object", + required: [ + "id", + "title", + "description", + "is_singapore", + "date", + "categories", + "analysises", + "gp_questions", + ], + title: "EventDTO", +} as const; + +export const EventIndexResponseSchema = { + properties: { + total_count: { + type: "integer", + title: "Total Count", + }, + count: { + type: "integer", + title: "Count", + }, + data: { + items: { + $ref: "#/components/schemas/MiniEventDTO", + }, + type: "array", + title: "Data", + }, + }, + type: "object", + required: ["total_count", "count", "data"], + title: "EventIndexResponse", +} as const; + +export const GPQuestionDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + question: { + type: "string", + title: "Question", + }, + is_llm_generated: { + type: "boolean", + title: "Is Llm Generated", + }, + categories: { + items: { + $ref: "#/components/schemas/CategoryDTO", + }, + type: "array", + title: "Categories", + }, + }, + type: "object", + required: ["id", "question", "is_llm_generated", "categories"], + title: "GPQuestionDTO", +} as const; + export const HTTPValidationErrorSchema = { properties: { detail: { @@ -69,6 +244,91 @@ export const HTTPValidationErrorSchema = { title: "HTTPValidationError", } as const; +export const MiniEventDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + title: { + type: "string", + title: "Title", + }, + description: { + type: "string", + title: "Description", + }, + is_singapore: { + type: "boolean", + title: "Is Singapore", + }, + date: { + type: "string", + format: "date-time", + title: "Date", + }, + categories: { + items: { + $ref: "#/components/schemas/CategoryDTO", + }, + type: "array", + title: "Categories", + }, + }, + type: "object", + required: [ + "id", + "title", + "description", + "is_singapore", + "date", + "categories", + ], + title: "MiniEventDTO", +} as const; + +export const PointMiniDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + title: { + type: "string", + title: "Title", + }, + body: { + type: "string", + title: "Body", + }, + events: { + items: { + $ref: "#/components/schemas/EventDTO", + }, + type: "array", + title: "Events", + }, + }, + type: "object", + required: ["id", "title", "body", "events"], + title: "PointMiniDTO", +} as const; + +export const ProfileUpdateSchema = { + properties: { + category_ids: { + items: { + type: "integer", + }, + type: "array", + title: "Category Ids", + }, + }, + type: "object", + required: ["category_ids"], + title: "ProfileUpdate", +} as const; + export const SignUpDataSchema = { properties: { email: { @@ -117,12 +377,38 @@ export const UserPublicSchema = { format: "email", title: "Email", }, + categories: { + items: { + $ref: "#/components/schemas/CategoryDTO", + }, + type: "array", + title: "Categories", + }, }, type: "object", - required: ["id", "email"], + required: ["id", "email", "categories"], title: "UserPublic", } as const; +export const UserQuestionMiniDTOSchema = { + properties: { + id: { + type: "integer", + title: "Id", + }, + question: { + type: "string", + title: "Question", + }, + answer: { + $ref: "#/components/schemas/AnswerDTO", + }, + }, + type: "object", + required: ["id", "question", "answer"], + title: "UserQuestionMiniDTO", +} as const; + export const ValidationErrorSchema = { properties: { loc: { diff --git a/frontend/client/services.gen.ts b/frontend/client/services.gen.ts index f65c9cd9..39abc72b 100644 --- a/frontend/client/services.gen.ts +++ b/frontend/client/services.gen.ts @@ -10,9 +10,26 @@ import type { AuthGoogleAuthGoogleGetData, AuthGoogleAuthGoogleGetError, AuthGoogleAuthGoogleGetResponse, + CreateUserQuestionUserQuestionsPostData, + CreateUserQuestionUserQuestionsPostError, + CreateUserQuestionUserQuestionsPostResponse, + GetCategoriesCategoriesGetError, + GetCategoriesCategoriesGetResponse, + GetEventEventsIdGetData, + GetEventEventsIdGetError, + GetEventEventsIdGetResponse, + GetEventsEventsGetData, + GetEventsEventsGetError, + GetEventsEventsGetResponse, GetUserAuthSessionGetData, GetUserAuthSessionGetError, GetUserAuthSessionGetResponse, + GetUserQuestionsUserQuestionsGetData, + GetUserQuestionsUserQuestionsGetError, + GetUserQuestionsUserQuestionsGetResponse, + GetUserQuestionUserQuestionsIdGetData, + GetUserQuestionUserQuestionsIdGetError, + GetUserQuestionUserQuestionsIdGetResponse, LogInAuthLoginPostData, LogInAuthLoginPostError, LogInAuthLoginPostResponse, @@ -23,6 +40,9 @@ import type { SignUpAuthSignupPostData, SignUpAuthSignupPostError, SignUpAuthSignupPostResponse, + UpdateProfileProfilePutData, + UpdateProfileProfilePutError, + UpdateProfileProfilePutResponse, } from "./types.gen"; export const client = createClient(createConfig()); @@ -129,3 +149,123 @@ export const logoutAuthLogoutGet = ( url: "/auth/logout", }); }; + +/** + * Get Categories + */ +export const getCategoriesCategoriesGet = < + ThrowOnError extends boolean = false, +>( + options?: Options, +) => { + return (options?.client ?? client).get< + GetCategoriesCategoriesGetResponse, + GetCategoriesCategoriesGetError, + ThrowOnError + >({ + ...options, + url: "/categories/", + }); +}; + +/** + * Update Profile + */ +export const updateProfileProfilePut = ( + options: Options, +) => { + return (options?.client ?? client).put< + UpdateProfileProfilePutResponse, + UpdateProfileProfilePutError, + ThrowOnError + >({ + ...options, + url: "/profile/", + }); +}; + +/** + * Get Events + */ +export const getEventsEventsGet = ( + options?: Options, +) => { + return (options?.client ?? client).get< + GetEventsEventsGetResponse, + GetEventsEventsGetError, + ThrowOnError + >({ + ...options, + url: "/events/", + }); +}; + +/** + * Get Event + */ +export const getEventEventsIdGet = ( + options: Options, +) => { + return (options?.client ?? client).get< + GetEventEventsIdGetResponse, + GetEventEventsIdGetError, + ThrowOnError + >({ + ...options, + url: "/events/:id", + }); +}; + +/** + * Get User Questions + */ +export const getUserQuestionsUserQuestionsGet = < + ThrowOnError extends boolean = false, +>( + options?: Options, +) => { + return (options?.client ?? client).get< + GetUserQuestionsUserQuestionsGetResponse, + GetUserQuestionsUserQuestionsGetError, + ThrowOnError + >({ + ...options, + url: "/user-questions/", + }); +}; + +/** + * Create User Question + */ +export const createUserQuestionUserQuestionsPost = < + ThrowOnError extends boolean = false, +>( + options: Options, +) => { + return (options?.client ?? client).post< + CreateUserQuestionUserQuestionsPostResponse, + CreateUserQuestionUserQuestionsPostError, + ThrowOnError + >({ + ...options, + url: "/user-questions/", + }); +}; + +/** + * Get User Question + */ +export const getUserQuestionUserQuestionsIdGet = < + ThrowOnError extends boolean = false, +>( + options: Options, +) => { + return (options?.client ?? client).get< + GetUserQuestionUserQuestionsIdGetResponse, + GetUserQuestionUserQuestionsIdGetError, + ThrowOnError + >({ + ...options, + url: "/user-questions/:id", + }); +}; diff --git a/frontend/client/types.gen.ts b/frontend/client/types.gen.ts index a4e57709..85a6f1a4 100644 --- a/frontend/client/types.gen.ts +++ b/frontend/client/types.gen.ts @@ -1,5 +1,16 @@ // This file is auto-generated by @hey-api/openapi-ts +export type AnalysisDTO = { + id: number; + category: CategoryDTO; + content: string; +}; + +export type AnswerDTO = { + id: number; + points: Array; +}; + export type Body_log_in_auth_login_post = { grant_type?: string | null; username: string; @@ -9,10 +20,63 @@ export type Body_log_in_auth_login_post = { client_secret?: string | null; }; +export type CategoryDTO = { + id: number; + name: string; +}; + +export type CreateUserQuestion = { + question: string; +}; + +export type EventDTO = { + id: number; + title: string; + description: string; + is_singapore: boolean; + date: string; + categories: Array; + analysises: Array; + gp_questions: Array; +}; + +export type EventIndexResponse = { + total_count: number; + count: number; + data: Array; +}; + +export type GPQuestionDTO = { + id: number; + question: string; + is_llm_generated: boolean; + categories: Array; +}; + export type HTTPValidationError = { detail?: Array; }; +export type MiniEventDTO = { + id: number; + title: string; + description: string; + is_singapore: boolean; + date: string; + categories: Array; +}; + +export type PointMiniDTO = { + id: number; + title: string; + body: string; + events: Array; +}; + +export type ProfileUpdate = { + category_ids: Array; +}; + export type SignUpData = { email: string; password: string; @@ -27,6 +91,13 @@ export type Token = { export type UserPublic = { id: number; email: string; + categories: Array; +}; + +export type UserQuestionMiniDTO = { + id: number; + question: string; + answer: AnswerDTO; }; export type ValidationError = { @@ -74,3 +145,64 @@ export type GetUserAuthSessionGetError = HTTPValidationError; export type LogoutAuthLogoutGetResponse = unknown; export type LogoutAuthLogoutGetError = unknown; + +export type GetCategoriesCategoriesGetResponse = Array; + +export type GetCategoriesCategoriesGetError = unknown; + +export type UpdateProfileProfilePutData = { + body: ProfileUpdate; +}; + +export type UpdateProfileProfilePutResponse = UserPublic; + +export type UpdateProfileProfilePutError = HTTPValidationError; + +export type GetEventsEventsGetData = { + query?: { + category_ids?: Array | null; + end_date?: string | null; + limit?: number | null; + offset?: number | null; + start_date?: string | null; + }; +}; + +export type GetEventsEventsGetResponse = EventIndexResponse; + +export type GetEventsEventsGetError = HTTPValidationError; + +export type GetEventEventsIdGetData = { + query: { + id: number; + }; +}; + +export type GetEventEventsIdGetResponse = EventDTO; + +export type GetEventEventsIdGetError = HTTPValidationError; + +export type GetUserQuestionsUserQuestionsGetData = unknown; + +export type GetUserQuestionsUserQuestionsGetResponse = + Array; + +export type GetUserQuestionsUserQuestionsGetError = HTTPValidationError; + +export type CreateUserQuestionUserQuestionsPostData = { + body: CreateUserQuestion; +}; + +export type CreateUserQuestionUserQuestionsPostResponse = UserQuestionMiniDTO; + +export type CreateUserQuestionUserQuestionsPostError = HTTPValidationError; + +export type GetUserQuestionUserQuestionsIdGetData = { + query: { + id: number; + }; +}; + +export type GetUserQuestionUserQuestionsIdGetResponse = unknown; + +export type GetUserQuestionUserQuestionsIdGetError = HTTPValidationError; diff --git a/frontend/components/auth/user-profile-button.tsx b/frontend/components/auth/user-profile-button.tsx index f422e9e7..1dc30666 100644 --- a/frontend/components/auth/user-profile-button.tsx +++ b/frontend/components/auth/user-profile-button.tsx @@ -1,6 +1,15 @@ +import { createElement, useState } from "react"; import { useRouter } from "next/navigation"; +import { + ChevronsDownUpIcon, + ChevronsUpDownIcon, + CreditCardIcon, + LogOutIcon, + UserIcon, +} from "lucide-react"; import { logoutAuthLogoutGet } from "@/client"; +import Chip from "@/components/display/chip"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, @@ -11,38 +20,72 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Skeleton } from "@/components/ui/skeleton"; import { useUserStore } from "@/store/user/user-store-provider"; import { getInitialFromEmail, getNameFromEmail } from "@/utils/string"; const UserProfileButton = () => { - const { email, setNotLoggedIn } = useUserStore((state) => state); const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const { user, setNotLoggedIn } = useUserStore((state) => state); const signout = async () => { await logoutAuthLogoutGet({ withCredentials: true }); setNotLoggedIn(); + router.push("/"); }; + if (!user) { + return ( +
+
+ +
+ + +
+
+
+ ); + } + + const email = user.email; return ( -
- +
+ setIsOpen(isOpen)}> - - {getInitialFromEmail(email)} - +
+
+ + {getInitialFromEmail(email)} + +
+

{getNameFromEmail(email)}

+

{email}

+
+
+ {createElement(isOpen ? ChevronsDownUpIcon : ChevronsUpDownIcon, { + className: "h-4 w-4 ml-auto shrink-0 opacity-50", + })} +
- - -
-

{getNameFromEmail(email)}

-

{email}

-
-
+ + My account router.push("/user/profile")}> - Manage my profile + + Profile settings + + router.push("/user/profile")}> + + Manage billings + @@ -51,7 +94,8 @@ const UserProfileButton = () => { className="text-destructive focus:bg-destructive/10 focus:text-destructive" onClick={signout} > - Log out + + Log out diff --git a/frontend/components/display/chip.tsx b/frontend/components/display/chip.tsx new file mode 100644 index 00000000..84643b08 --- /dev/null +++ b/frontend/components/display/chip.tsx @@ -0,0 +1,45 @@ +import { cva, VariantProps } from "class-variance-authority"; +import { LucideIcon } from "lucide-react"; + +import { Box } from "@/components/ui/box"; +import { cn } from "@/lib/utils"; + +const chipVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-xs font-medium transition-colors disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-sky-200/60 text-sky-600 hover:bg-sky-200/40", + destructive: "bg-red-200/60 text-destructive/90 hover:bg-red-200/40", + green: "bg-[#C5EBD9]/60 text-[#2D835A] hover:bg-[#C5EBD9]/40", + greygreen: "bg-[#D2DAD6]/60 text-[#4C6559] hover:bg-[#D2DAD6]/40", + }, + size: { + default: "h-6 px-2 py-2", + sm: "h-5 rounded-md px-2", + lg: "h-7 rounded-md px-3 text-sm", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +interface ChipProps extends VariantProps { + label: string; + className?: string; + Icon?: LucideIcon; +} + +const Chip = ({ label, variant, size, className, Icon }: ChipProps) => { + return ( + + {Icon && } + {label} + + ); +}; + +export default Chip; diff --git a/frontend/components/form/inputs/password-input.tsx b/frontend/components/form/inputs/password-input.tsx index fd50e8f8..2a156f80 100644 --- a/frontend/components/form/inputs/password-input.tsx +++ b/frontend/components/form/inputs/password-input.tsx @@ -28,7 +28,7 @@ const PasswordInput = forwardRef(

{helperText}

)}
-
+
+> = { + [MediaBreakpoint.Sm]: { + defaultSize: 25, + maxSize: 60, + minSize: 20, + collapsible: true, + collapsedSize: 1, + }, + [MediaBreakpoint.Md]: { + defaultSize: 20, + maxSize: 40, + minSize: 10, + collapsible: true, + collapsedSize: 1, + }, + [MediaBreakpoint.Lg]: { + defaultSize: 20, + maxSize: 40, + minSize: 20, + collapsible: true, + collapsedSize: 1, + }, + [MediaBreakpoint.Xl]: { + defaultSize: 16, + maxSize: 35, + minSize: 15, + collapsible: true, + collapsedSize: 1, + }, + [MediaBreakpoint.Xxl]: { + defaultSize: 10, + maxSize: 25, + minSize: 15, + collapsible: true, + collapsedSize: 1, + }, + [MediaBreakpoint.Xxxl]: { + defaultSize: 8, + maxSize: 25, + minSize: 5, + collapsible: true, + collapsedSize: 1, + }, +}; const AppLayout = ({ children }: { children: ReactNode }) => { - const { setLoggedIn, setNotLoggedIn } = useUserStore((state) => state); + const mediaBreakpoint = useBreakpointMediaQuery(); + const [isCollapsed, setIsCollapsed] = useState(false); + const { isLoggedIn, setLoggedIn, setNotLoggedIn } = useUserStore( + (state) => state, + ); const { data: userProfile, isSuccess: isUserProfileSuccess } = useQuery(getUserProfile()); useEffect(() => { if (isUserProfileSuccess && userProfile) { - setLoggedIn(userProfile.id, userProfile.email); + setLoggedIn(userProfile); } else { setNotLoggedIn(); } @@ -24,7 +84,33 @@ const AppLayout = ({ children }: { children: ReactNode }) => { return (
-
{children}
+
+ + {isLoggedIn && ( + <> + setIsCollapsed(true)} + onExpand={() => setIsCollapsed(false)} + order={1} + {...breakpointConfigMap[mediaBreakpoint]} + > + + + + + )} + +
+ {children} +
+
+
+
); diff --git a/frontend/components/navigation/navbar.tsx b/frontend/components/navigation/navbar.tsx index 780d53e7..2b3a0888 100644 --- a/frontend/components/navigation/navbar.tsx +++ b/frontend/components/navigation/navbar.tsx @@ -1,5 +1,4 @@ import { useEffect } from "react"; -import Link from "next/link"; import { NavigationMenu, NavigationMenuItem, @@ -8,12 +7,16 @@ import { } from "@radix-ui/react-navigation-menu"; import { useQuery } from "@tanstack/react-query"; -import UserProfileButton from "@/components/auth/user-profile-button"; +import JippyIcon from "@/assets/jippy-icon/jippy-icon-sm"; +import JippyLogo from "@/assets/jippy-logo/jippy-logo-sm"; +import { Button } from "@/components/ui/button"; import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import { getUserProfile } from "@/queries/user"; import { useUserStore } from "@/store/user/user-store-provider"; import { NavItem } from "@/types/navigation"; +import Link from "./link"; + export const NavItems: NavItem[] = []; function Navbar() { @@ -25,20 +28,19 @@ function Navbar() { useEffect(() => { if (isUserProfileSuccess && userProfile) { - setLoggedIn(userProfile.id, userProfile.email); + setLoggedIn(userProfile); } else { setNotLoggedIn(); } }, [userProfile, isUserProfileSuccess, setLoggedIn, setNotLoggedIn]); return ( -
-
+
+
- - {process.env.NEXT_PUBLIC_APP_NAME} - + + @@ -56,7 +58,20 @@ function Navbar() {
- {isLoggedIn && } + {!isLoggedIn && ( +
+ +
+ )}
); diff --git a/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx b/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx new file mode 100644 index 00000000..ab7d614c --- /dev/null +++ b/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx @@ -0,0 +1,23 @@ +import { LucideIcon } from "lucide-react"; + +interface SidebarItemWithIconProps { + Icon: LucideIcon; + label: string; +} + +const SidebarItemWithIcon = ({ Icon, label }: SidebarItemWithIconProps) => { + return ( +
+ + + {label} + +
+ ); +}; + +export default SidebarItemWithIcon; diff --git a/frontend/components/navigation/sidebar/sidebar-other-topics.tsx b/frontend/components/navigation/sidebar/sidebar-other-topics.tsx new file mode 100644 index 00000000..6bb3765e --- /dev/null +++ b/frontend/components/navigation/sidebar/sidebar-other-topics.tsx @@ -0,0 +1,46 @@ +import { + categoriesToDisplayName, + categoriesToIconsMap, + Category, +} from "@/types/categories"; + +import SidebarItemWithIcon from "./sidebar-item-with-icon"; + +// TODO: dynamically fetch +const otherTopics = [ + Category.SciTech, + Category.ArtsHumanities, + Category.Politics, + Category.Media, + Category.Environment, + Category.Economics, + Category.Sports, + Category.GenderEquality, + Category.Religion, + Category.SocietyCulture, +]; + +const SidebarOtherTopics = () => { + return ( +
+

+ Other topics +

+
+ {otherTopics.map((category) => { + const categoryLabel = categoriesToDisplayName[category]; + const categoryIcon = categoriesToIconsMap[category]; + return ( + + ); + })} +
+
+ ); +}; + +export default SidebarOtherTopics; diff --git a/frontend/components/navigation/sidebar/sidebar.tsx b/frontend/components/navigation/sidebar/sidebar.tsx new file mode 100644 index 00000000..546abbf0 --- /dev/null +++ b/frontend/components/navigation/sidebar/sidebar.tsx @@ -0,0 +1,14 @@ +import UserProfileButton from "@/components/auth/user-profile-button"; +import SidebarOtherTopics from "@/components/navigation/sidebar/sidebar-other-topics"; + +/* Assumption: This component is only rendered if the user is logged in */ +const Sidebar = () => { + return ( +
+ + +
+ ); +}; + +export default Sidebar; diff --git a/frontend/components/news/news-article.tsx b/frontend/components/news/news-article.tsx new file mode 100644 index 00000000..439a89a7 --- /dev/null +++ b/frontend/components/news/news-article.tsx @@ -0,0 +1,63 @@ +import Image from "next/image"; +import { ArrowUpRightIcon } from "lucide-react"; + +import Chip from "@/components/display/chip"; +import { + categoriesToDisplayName, + categoriesToIconsMap, + Category, +} from "@/types/categories"; + +const sampleArticleCategories = [ + Category.Economics, + Category.Environment, + Category.Media, + Category.Politics, +]; + +const NewsArticle = () => { + return ( +
+
+
+ + CNA, Guardian + + 21 Sep 2024 +
+

+ Norris Claims Singapore GP Pole Amid Ferrari’s Setback +

+

+ A Reflection on Commercialization, Sustainability, and Global Sports + as Cultural Forces +

+
+ {sampleArticleCategories.map((category) => ( + + ))} +
+
+
+ +
+
+ ); +}; + +export default NewsArticle; diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx index 13fb9acd..a61bd2fa 100644 --- a/frontend/components/ui/avatar.tsx +++ b/frontend/components/ui/avatar.tsx @@ -11,7 +11,7 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( (({ className, ...props }, ref) => ( ) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx new file mode 100644 index 00000000..91566d73 --- /dev/null +++ b/frontend/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + ref={ref} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx new file mode 100644 index 00000000..2cdf440d --- /dev/null +++ b/frontend/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/frontend/hooks/use-breakpoint-media-query.ts b/frontend/hooks/use-breakpoint-media-query.ts new file mode 100644 index 00000000..af784809 --- /dev/null +++ b/frontend/hooks/use-breakpoint-media-query.ts @@ -0,0 +1,23 @@ +import { useMediaQuery } from "usehooks-ts"; + +import { MediaBreakpoint } from "@/utils/media"; + +function useBreakpointMediaQuery(): MediaBreakpoint { + const isMdBreakpoint = useMediaQuery(`(min-width: ${MediaBreakpoint.Md})`); + const isLgBreakpoint = useMediaQuery(`(min-width: ${MediaBreakpoint.Lg})`); + const isXlBreakpoint = useMediaQuery(`(min-width: ${MediaBreakpoint.Xl})`); + const isXxlBreakpoint = useMediaQuery(`(min-width: ${MediaBreakpoint.Xxl})`); + const isXxxlBreakpoint = useMediaQuery( + `(min-width: ${MediaBreakpoint.Xxxl})`, + ); + + if (isXxxlBreakpoint) return MediaBreakpoint.Xxxl; + if (isXxlBreakpoint) return MediaBreakpoint.Xxl; + if (isXlBreakpoint) return MediaBreakpoint.Xl; + if (isMdBreakpoint) return MediaBreakpoint.Md; + if (isLgBreakpoint) return MediaBreakpoint.Lg; + + return MediaBreakpoint.Sm; +} + +export default useBreakpointMediaQuery; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2447282d..d1edeb49 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@tanstack/react-query": "^5.56.2", @@ -28,8 +29,10 @@ "react-dom": "^18", "react-ga4": "^2.1.0", "react-hook-form": "^7.53.0", + "react-resizable-panels": "^2.1.3", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.0", "zod": "^3.23.8", "zustand": "^5.0.0-rc.2" }, @@ -1073,6 +1076,12 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", @@ -1613,6 +1622,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", + "integrity": "sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -5333,6 +5385,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6535,6 +6593,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.3.tgz", + "integrity": "sha512-Zz0sCro6aUubL+hYh67eTnn5vxAu+HUZ7+IXvGjsBCBaudDEpIyZyDGE3vcgKi2w6IN3rYH+WXO+MwpgMSOpaQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -7681,6 +7749,21 @@ } } }, + "node_modules/usehooks-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", + "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 668d734f..c54ac812 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@tanstack/react-query": "^5.56.2", @@ -30,8 +31,10 @@ "react-dom": "^18", "react-ga4": "^2.1.0", "react-hook-form": "^7.53.0", + "react-resizable-panels": "^2.1.3", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.0", "zod": "^3.23.8", "zustand": "^5.0.0-rc.2" }, diff --git a/frontend/store/user/user-store.ts b/frontend/store/user/user-store.ts index 6ad7209d..7e54a067 100644 --- a/frontend/store/user/user-store.ts +++ b/frontend/store/user/user-store.ts @@ -1,9 +1,10 @@ import { createStore } from "zustand"; +import { UserPublic } from "@/client"; + interface UserState { isLoggedIn: boolean; - userId?: number; - email?: string; + user?: UserPublic; } export const defaultUserState: UserState = { @@ -11,7 +12,7 @@ export const defaultUserState: UserState = { }; interface UserActions { - setLoggedIn: (userId?: number, email?: string) => void; + setLoggedIn: (user: UserPublic) => void; setNotLoggedIn: () => void; } @@ -20,13 +21,11 @@ export type UserStore = UserState & UserActions; export const createUserStore = (initState: UserState = defaultUserState) => { return createStore()((set) => ({ ...initState, - setLoggedIn: (userId, email) => - set(() => ({ isLoggedIn: true, userId, email })), + setLoggedIn: (user) => set(() => ({ isLoggedIn: true, user })), setNotLoggedIn: () => set(() => ({ isLoggedIn: false, - userId: undefined, - email: undefined, + user: undefined, })), })); }; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 21740ac7..c08d88a0 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -50,6 +50,7 @@ const config: Config = { "4": "hsl(var(--chart-4))", "5": "hsl(var(--chart-5))", }, + offblack: "#373530", }, borderRadius: { lg: "var(--radius)", diff --git a/frontend/types/categories.ts b/frontend/types/categories.ts new file mode 100644 index 00000000..3bc2145b --- /dev/null +++ b/frontend/types/categories.ts @@ -0,0 +1,52 @@ +import { + Building2, + DollarSign, + Film, + HeartHandshake, + Leaf, + LucideIcon, + Medal, + Microscope, + Palette, + Scale, + UsersRound, +} from "lucide-react"; + +export enum Category { + SciTech = "Science & technology", + ArtsHumanities = "Arts & humanities", + Politics = "Politics", + Media = "Media", + Environment = "Environment", + Economics = "Economics", + Sports = "Sports", + GenderEquality = "Gender & equality", + Religion = "Religion", + SocietyCulture = "Society & culture", +} + +export const categoriesToDisplayName: Record = { + [Category.SciTech]: "Science & technology", + [Category.ArtsHumanities]: "Arts & humanities", + [Category.Politics]: "Politics", + [Category.Media]: "Media", + [Category.Environment]: "Environment", + [Category.Economics]: "Economics", + [Category.Sports]: "Sports", + [Category.GenderEquality]: "Gender & equality", + [Category.Religion]: "Religion", + [Category.SocietyCulture]: "Society & culture", +}; + +export const categoriesToIconsMap: Record = { + [Category.SciTech]: Microscope, + [Category.ArtsHumanities]: Palette, + [Category.Politics]: Building2, + [Category.Media]: Film, + [Category.Environment]: Leaf, + [Category.Economics]: DollarSign, + [Category.Sports]: Medal, + [Category.GenderEquality]: Scale, + [Category.Religion]: HeartHandshake, + [Category.SocietyCulture]: UsersRound, +}; diff --git a/frontend/utils/media.ts b/frontend/utils/media.ts new file mode 100644 index 00000000..cdc093ee --- /dev/null +++ b/frontend/utils/media.ts @@ -0,0 +1,8 @@ +export enum MediaBreakpoint { + Sm = "640px", + Md = "768px", + Lg = "1024px", + Xl = "1280px", + Xxl = "1920px", + Xxxl = "2560px", +}