diff --git a/src/app/groups/didattica/[school]/[level]/[course]/page.tsx b/src/app/groups/didattica/[school]/[level]/[course]/page.tsx new file mode 100644 index 0000000..ac593d4 --- /dev/null +++ b/src/app/groups/didattica/[school]/[level]/[course]/page.tsx @@ -0,0 +1,10 @@ +import { GroupsResult } from "@/components/groups/groups-result" + +export default async function DidatticaGroupsResultPage({ + params, +}: { + params: Promise<{ school: string; level: string; course: string }> +}) { + const { school, level, course } = await params + return +} diff --git a/src/app/groups/didattica/[school]/[level]/page.tsx b/src/app/groups/didattica/[school]/[level]/page.tsx new file mode 100644 index 0000000..251d1a8 --- /dev/null +++ b/src/app/groups/didattica/[school]/[level]/page.tsx @@ -0,0 +1,14 @@ +import { CourseStep } from "@/components/groups/course-step" +import type { CourseSearchParams } from "@/components/groups/types" + +export default async function DidatticaCoursePage({ + params, + searchParams, +}: { + params: Promise<{ school: string; level: string }> + searchParams: Promise +}) { + const { school, level } = await params + const { campus, lang } = await searchParams + return +} diff --git a/src/app/groups/didattica/[school]/page.tsx b/src/app/groups/didattica/[school]/page.tsx new file mode 100644 index 0000000..28beb6b --- /dev/null +++ b/src/app/groups/didattica/[school]/page.tsx @@ -0,0 +1,6 @@ +import { LevelStep } from "@/components/groups/level-step" + +export default async function DidatticaLevelPage({ params }: { params: Promise<{ school: string }> }) { + const { school } = await params + return +} diff --git a/src/app/groups/didattica/page.tsx b/src/app/groups/didattica/page.tsx new file mode 100644 index 0000000..e944d36 --- /dev/null +++ b/src/app/groups/didattica/page.tsx @@ -0,0 +1,5 @@ +import { SchoolStep } from "@/components/groups/school-step" + +export default function DidatticaWizard() { + return +} diff --git a/src/app/groups/extra/page.tsx b/src/app/groups/extra/page.tsx new file mode 100644 index 0000000..532abf0 --- /dev/null +++ b/src/app/groups/extra/page.tsx @@ -0,0 +1,5 @@ +import { ExtraGroups } from "@/components/groups/extra-groups" + +export default function ExtraGroupsPage() { + return +} diff --git a/src/app/groups/page.tsx b/src/app/groups/page.tsx new file mode 100644 index 0000000..941e5be --- /dev/null +++ b/src/app/groups/page.tsx @@ -0,0 +1,51 @@ +import { FiBook, FiStar } from "react-icons/fi" +import { CardIcon } from "@/components/card-icon" +import { GroupSearch } from "@/components/home/group-search" + +const groups = [ + { + title: "Gruppi Didattici", + caption: "Ingegneria, Architettura, Design, e tutti i gruppi del tuo corso di studi.", + href: "/groups/didattica", + icon: FiBook, + cta: "Inizia", + }, + { + title: "Gruppi Extra", + caption: "Affitti, mercatino, eventi, hobby e tutto ciò che riguarda la vita studentesca.", + href: "/groups/extra", + icon: FiStar, + cta: "Esplora", + }, +] as const + +export default function Home() { + return ( +
+
+
+

+ Groups +

+ +
+ +
+ {groups.map((group) => ( + + ))} +
+
+
+ ) +} diff --git a/src/components/button-icon.tsx b/src/components/button-icon.tsx index 62ef264..052fe41 100644 --- a/src/components/button-icon.tsx +++ b/src/components/button-icon.tsx @@ -1,25 +1,27 @@ -import type { ReactNode } from "react" +import type { ComponentProps } from "react" import type { IconType } from "react-icons" -import { Button, type ButtonSizes } from "./ui/button" +import { Button } from "./ui/button" + +type ButtonIconProps = ComponentProps & { + icon: IconType + text?: string + iconPosition?: "left" | "right" + iconClassName?: string +} export function ButtonIcon({ - variant = "primary", icon: Icon, - children, + text, iconPosition = "left", + iconClassName, size, -}: { - variant?: "primary" | "tertiary" | "tertiaryBlur" - icon: IconType - children: ReactNode - iconPosition?: "left" | "right" - size: ButtonSizes -}) { + ...props +}: ButtonIconProps) { return ( - ) } diff --git a/src/components/card-course-group.tsx b/src/components/card-course-group.tsx index 46cb821..35e5fc6 100644 --- a/src/components/card-course-group.tsx +++ b/src/components/card-course-group.tsx @@ -6,7 +6,7 @@ import { cn } from "@/lib/utils" import { Card, CardAction, CardTitle } from "./ui/card" export const cardCourseGroupVariants = cva( - "flex h-fit w-full flex-row items-center gap-5 px-7.5 py-6.25 font-normal leading-6 tracking-[0.03125rem]", + "flex h-fit w-full min-w-0 flex-1 flex-row items-center gap-3 px-5 py-4 font-normal leading-6 tracking-[0.03125rem] sm:gap-5 sm:px-7.5 sm:py-6.25", { variants: { secondary: { @@ -27,21 +27,30 @@ export function CardCourseGroup({ hasTelegram = true, iconTelegram: IconTelegram = LiaTelegramPlane, secondary = false, + stacked = false, }: { groupName: string hasWhatsapp?: boolean iconWhatsApp?: IconType hasTelegram?: boolean iconTelegram?: IconType + stacked?: boolean } & VariantProps) { - const actionClassName = cn("rounded-full p-3.75", secondary ? "bg-[#51A2FF]" : "bg-[#74D4FF]") + const actionClassName = cn("rounded-full p-2 sm:p-3.75", secondary ? "bg-[#51A2FF]" : "bg-[#74D4FF]") return ( - - + + {groupName} - {hasWhatsapp && } - {hasTelegram && } +
+ {hasWhatsapp && } + {hasTelegram && } +
) } diff --git a/src/components/card-course.tsx b/src/components/card-course.tsx index 672cea2..fdad659 100644 --- a/src/components/card-course.tsx +++ b/src/components/card-course.tsx @@ -1,16 +1,14 @@ import type { IconType } from "react-icons" -import { CiGlobe } from "react-icons/ci" -import { FaAngleRight } from "react-icons/fa6" -import { IoLocationOutline } from "react-icons/io5" +import { FiChevronRight, FiGlobe, FiMapPin } from "react-icons/fi" import { Card, CardAction, CardContent } from "./ui/card" export function CardCourse({ courseName, - iconLocation: IconLocation = IoLocationOutline, + iconLocation: IconLocation = FiMapPin, location = "Milano Leonardo", - iconLanguage: IconLanguage = CiGlobe, + iconLanguage: IconLanguage = FiGlobe, language = "ITA", - iconSelect: IconSelect = FaAngleRight, + iconSelect: IconSelect = FiChevronRight, }: { courseName: string iconLocation?: IconType @@ -20,16 +18,20 @@ export function CardCourse({ iconSelect?: IconType }) { return ( - - {courseName} - - {location} - - - {language} - - - + +
+ {courseName} +
+ + {location} + + + {language} + +
+
+ +
) diff --git a/src/components/card-icon/index.tsx b/src/components/card-icon/index.tsx index a38598f..27bba12 100644 --- a/src/components/card-icon/index.tsx +++ b/src/components/card-icon/index.tsx @@ -1,4 +1,5 @@ import { Glass } from "@/components/glass" +import { buttonVariants } from "@/components/ui/button" import { cn } from "@/lib/utils" import { BasicCardMedia } from "./basic-card-media" import { DescriptionCardMedia } from "./description-card-media" @@ -10,6 +11,7 @@ import { getAlignmentClasses, getCardPaddingClasses, getContentGapClasses, getTi export function CardIcon(props: CardIconProps) { const { title, icon, size = "md", href, hoverEffect = false, align = "center", className } = props const description = "description" in props ? props.description : undefined + const cta = "cta" in props ? props.cta : undefined const Root = href ? "a" : "div" const isDescriptionCard = Boolean(description) const isInlineAligned = align === "inline" @@ -78,6 +80,12 @@ export function CardIcon(props: CardIconProps) {

)} + + {cta && ( +
+ {cta} +
+ )} )} diff --git a/src/components/card-icon/types.ts b/src/components/card-icon/types.ts index f4ff7b3..3f0f9b4 100644 --- a/src/components/card-icon/types.ts +++ b/src/components/card-icon/types.ts @@ -27,5 +27,6 @@ export type SharedCardProps = { } export type CardIconProps = SharedCardProps & { + cta?: string description?: ReactNode } diff --git a/src/components/card-path-selection.tsx b/src/components/card-path-selection.tsx index f615c3e..41356b5 100644 --- a/src/components/card-path-selection.tsx +++ b/src/components/card-path-selection.tsx @@ -2,9 +2,17 @@ import type { IconType } from "react-icons" import { BsBook } from "react-icons/bs" import { Card, CardAction, CardContent } from "./ui/card" -export function CardPathSelection({ caption, icon: Icon = BsBook }: { caption: string; icon?: IconType }) { +export function CardPathSelection({ + caption, + icon: Icon = BsBook, + className, +}: { + caption: string + icon?: IconType + className?: string +}) { return ( - + {caption} diff --git a/src/components/groups/constants.ts b/src/components/groups/constants.ts new file mode 100644 index 0000000..38f8ad6 --- /dev/null +++ b/src/components/groups/constants.ts @@ -0,0 +1,44 @@ +import type { Course, Level, School } from "@/components/groups/types" + +export const SCHOOLS: School[] = [ + { slug: "architettura", name: "Scuola di Architettura" }, + { slug: "design", name: "Scuola di Design" }, + { slug: "ingegneria", name: "Scuole di Ingegneria" }, +] + +export const LEVELS: Level[] = [ + { slug: "triennale", name: "Triennale" }, + { slug: "magistrale", name: "Magistrale" }, +] + +export const COURSES: Record = { + "design/triennale": [ + { slug: "design-prodotto", name: "Design del Prodotto", location: "Milano Bovisa", language: "ITA" }, + { slug: "design-comunicazione", name: "Design della Comunicazione", location: "Milano Bovisa", language: "ITA" }, + { slug: "design-moda", name: "Design della Moda", location: "Milano Bovisa", language: "ITA" }, + ], +} + +export function getSchool(slug: string) { + return SCHOOLS.find((school) => school.slug === slug) +} + +export function getLevel(slug: string) { + return LEVELS.find((level) => level.slug === slug) +} + +export function getCourses(school: string, level: string) { + return COURSES[`${school}/${level}`] ?? [] +} + +export function getCourse(school: string, level: string, slug: string) { + return getCourses(school, level).find((course) => course.slug === slug) +} + +export function getCourseFilters(school: string, level: string) { + const courses = getCourses(school, level) + return { + campuses: [...new Set(courses.map((course) => course.location))], + languages: [...new Set(courses.map((course) => course.language))], + } +} diff --git a/src/components/groups/course-filters.tsx b/src/components/groups/course-filters.tsx new file mode 100644 index 0000000..123a197 --- /dev/null +++ b/src/components/groups/course-filters.tsx @@ -0,0 +1,67 @@ +"use client" + +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +const ALL = "all" + +type FilterSelectProps = { + label: string + value: string + options: string[] + onChange: (value: string) => void +} + +function FilterSelect({ label, value, options, onChange }: FilterSelectProps) { + return ( + + ) +} + +export function CourseFilters({ campuses, languages }: { campuses: string[]; languages: string[] }) { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + + const update = (key: string, value: string) => { + const params = new URLSearchParams(searchParams.toString()) + if (value === ALL) params.delete(key) + else params.set(key, value) + const query = params.toString() + router.replace(query ? `${pathname}?${query}` : pathname) + } + + return ( +
+
+ update("campus", value)} + /> +
+
+ update("lang", value)} + /> +
+
+ ) +} diff --git a/src/components/groups/course-step.tsx b/src/components/groups/course-step.tsx new file mode 100644 index 0000000..7f4df44 --- /dev/null +++ b/src/components/groups/course-step.tsx @@ -0,0 +1,56 @@ +import Link from "next/link" +import { notFound } from "next/navigation" +import { FiChevronRight } from "react-icons/fi" +import { CardCourse } from "@/components/card-course" +import { getCourseFilters, getCourses, getLevel, getSchool } from "@/components/groups/constants" +import { CourseFilters } from "@/components/groups/course-filters" +import { WizardShell } from "@/components/groups/wizard-shell" +import { stepHref } from "../../utils/step-href" + +export function CourseStep({ + school: schoolSlug, + level, + campus, + lang, +}: { + school: string + level: string + campus?: string + lang?: string +}) { + const school = getSchool(schoolSlug) + const currentLevel = getLevel(level) + if (!school || !currentLevel) notFound() + + const { campuses, languages } = getCourseFilters(schoolSlug, level) + const courses = getCourses(schoolSlug, level).filter( + (course) => (!campus || course.location === campus) && (!lang || course.language === lang) + ) + + return ( + } + > +
+ {courses.length === 0 && ( +

Nessun corso corrisponde ai filtri.

+ )} + {courses.map((course) => ( + + + + ))} +
+
+ ) +} diff --git a/src/components/groups/extra-groups.tsx b/src/components/groups/extra-groups.tsx new file mode 100644 index 0000000..08b7ecc --- /dev/null +++ b/src/components/groups/extra-groups.tsx @@ -0,0 +1,31 @@ +import Link from "next/link" +import { FiArrowLeft } from "react-icons/fi" +import { CardCourseGroup } from "@/components/card-course-group" + +// TODO: placeholder da cambiare +const EXTRA_GROUPS_PLACEHOLDER = ["Affitti", "Mercatino", "Eventi", "Hobby"] + +export function ExtraGroups() { + return ( +
+
+ + + +
+

Gruppi Extra

+
+
+ +
+ {EXTRA_GROUPS_PLACEHOLDER.map((name) => ( + + ))} +
+
+ ) +} diff --git a/src/components/groups/groups-result.tsx b/src/components/groups/groups-result.tsx new file mode 100644 index 0000000..984c248 --- /dev/null +++ b/src/components/groups/groups-result.tsx @@ -0,0 +1,65 @@ +import Link from "next/link" +import { notFound } from "next/navigation" +import { FiArrowLeft, FiX } from "react-icons/fi" +import { CardCourseGroup } from "@/components/card-course-group" +import { getCourse, getLevel, getSchool } from "@/components/groups/constants" +import { stepHref } from "../../utils/step-href" + +// TODO: placeholder da cambiare +const FIRST_YEAR_GROUPS_PLACEHOLDER = Array.from({ length: 7 }, (_, i) => `Sezione P${i + 1}`) + +export function GroupsResult({ school: schoolSlug, level, course }: { school: string; level: string; course: string }) { + const school = getSchool(schoolSlug) + const currentCourse = getCourse(schoolSlug, level, course) + const currentLevel = getLevel(level) + if (!school || !currentCourse || !currentLevel) notFound() + + return ( +
+
+ + + +
+

{currentCourse.name}

+

+ Laurea {currentLevel.name} +

+
+ + + +
+ +
+ + +
+ +
+
+

+ Gruppi del Primo Anno +

+

+ {FIRST_YEAR_GROUPS_PLACEHOLDER.length} Gruppi +

+
+
+ {FIRST_YEAR_GROUPS_PLACEHOLDER.map((name) => ( + + ))} +
+
+
+ ) +} diff --git a/src/components/groups/level-step.tsx b/src/components/groups/level-step.tsx new file mode 100644 index 0000000..28d96d8 --- /dev/null +++ b/src/components/groups/level-step.tsx @@ -0,0 +1,30 @@ +import Link from "next/link" +import { notFound } from "next/navigation" +import { FiBook } from "react-icons/fi" +import { CardPathSelection } from "@/components/card-path-selection" +import { getSchool, LEVELS } from "@/components/groups/constants" +import { WizardShell } from "@/components/groups/wizard-shell" +import { stepHref } from "../../utils/step-href" + +export function LevelStep({ school: schoolSlug }: { school: string }) { + const school = getSchool(schoolSlug) + if (!school) notFound() + + return ( + +
+ {LEVELS.map((level) => ( + + + + ))} +
+
+ ) +} diff --git a/src/components/groups/school-step.tsx b/src/components/groups/school-step.tsx new file mode 100644 index 0000000..4c053fb --- /dev/null +++ b/src/components/groups/school-step.tsx @@ -0,0 +1,35 @@ +import type { IconType } from "react-icons" +import { FiBox, FiFeather, FiPenTool } from "react-icons/fi" +import { CardIcon } from "@/components/card-icon" +import { SCHOOLS } from "@/components/groups/constants" +import { WizardShell } from "@/components/groups/wizard-shell" +import { stepHref } from "../../utils/step-href" + +const SCHOOL_ICONS: Record = { + architettura: FiPenTool, + design: FiFeather, + ingegneria: FiBox, +} + +export function SchoolStep() { + return ( + +
+ {SCHOOLS.map((school) => ( + + ))} +
+
+ ) +} diff --git a/src/components/groups/types.ts b/src/components/groups/types.ts new file mode 100644 index 0000000..184edfb --- /dev/null +++ b/src/components/groups/types.ts @@ -0,0 +1,11 @@ +export type Campus = "Milano Leonardo" | "Milano Bovisa" | "Cremona" | "Lecco" | "Mantova" | "Piacenza" | "Xi'an" +export type Language = "ITA" | "ENG" + +export type School = { slug: string; name: string } +export type Level = { slug: string; name: string } +export type Course = { slug: string; name: string; location: Campus; language: Language } + +export type CourseSearchParams = { + campus?: string + lang?: string +} diff --git a/src/components/groups/wizard-shell.tsx b/src/components/groups/wizard-shell.tsx new file mode 100644 index 0000000..c0a7e75 --- /dev/null +++ b/src/components/groups/wizard-shell.tsx @@ -0,0 +1,96 @@ +"use client" + +import { useRouter } from "next/navigation" +import type { ReactNode } from "react" +import { FiArrowLeft, FiX } from "react-icons/fi" +import { ButtonIcon } from "@/components/button-icon" +import { Glass } from "@/components/glass" +import { cn } from "@/lib/utils" + +const STEPS = ["Scuola", "Triennale/​Magistrale", "Corso"] as const + +type WizardShellProps = { + activeStep: number + title: string + caption?: string + captionPosition?: "above" | "below" + backHref?: string + action?: ReactNode + children: ReactNode +} + +export function WizardShell({ + activeStep, + title, + caption, + captionPosition = "below", + backHref, + action, + children, +}: WizardShellProps) { + const router = useRouter() + + return ( +
+ +
+ (backHref ? router.replace(backHref) : router.back())} + /> + +
    + {STEPS.map((label, index) => ( +
  1. + {label} +
  2. + ))} +
+ + router.push("/groups")} + /> +
+ +
+
+
+ {captionPosition === "above" && caption && ( +

{caption}

+ )} +

+ {title} +

+ {captionPosition === "below" && caption && ( +

{caption}

+ )} +
+ {action &&
{action}
} +
+
{children}
+
+
+
+ ) +} diff --git a/src/components/home/group-search.tsx b/src/components/home/group-search.tsx index 53d3f41..a8f21f9 100644 --- a/src/components/home/group-search.tsx +++ b/src/components/home/group-search.tsx @@ -37,8 +37,8 @@ export function GroupSearch() { } type="text" - placeholder="Find your group" - aria-label="Find your group" + placeholder="Corso, Scuola, Interessi..." + aria-label="Corso, Scuola, Interessi..." containerClassName="w-full" className="typo-body-medium" value={query} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index c6cdca9..6f81785 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -28,6 +28,7 @@ const buttonVariants = cva( "bg-button-tertiary hover:bg-button-tertiary/90 text-text-accent-lightbg typo-label-large rounded-buttonsM", tertiaryBlur: "bg-blue-tertiary-blur hover:bg-blue-tertiary-blur/90 text-text-accent-lightbg typo-headline-small rounded-buttonsM", + glass: "bg-background-blur backdrop-blur-md rounded-buttonsL", outline: "border bg-white shadow-xs hover:bg-slate-100 hover:text-slate-900 dark:bg-slate-200/30 dark:border-slate-200 dark:hover:bg-slate-200/50 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:dark:bg-slate-800/30 dark:dark:border-slate-800 dark:dark:hover:bg-slate-800/50", link: "text-slate-900 underline-offset-4 underline dark:text-slate-50", diff --git a/src/styles/typography.css b/src/styles/typography.css index 3724904..0101893 100644 --- a/src/styles/typography.css +++ b/src/styles/typography.css @@ -68,6 +68,10 @@ @apply font-red-hat font-medium text-[0.6875rem]/[16px]; } +@utility typo-label-tiny { + @apply font-red-hat font-medium text-[0.59rem]/[15px]; +} + /* Title */ @utility typo-title-large { @apply font-poppins font-normal text-[1.375rem]/[28px]; diff --git a/src/utils/step-href.ts b/src/utils/step-href.ts new file mode 100644 index 0000000..bcb6949 --- /dev/null +++ b/src/utils/step-href.ts @@ -0,0 +1,4 @@ +export function stepHref(params: { school?: string; level?: string; course?: string }) { + const segments = [params.school, params.level, params.course].filter(Boolean) + return ["/groups/didattica", ...segments].join("/") +}