Skip to content

Commit

Permalink
Merge pull request #66 from flowcore-io/38-only-show-users-that-are-r…
Browse files Browse the repository at this point in the history
…elated-to-specific-conferences-and-have-tickets

Added an attendance page
  • Loading branch information
suuunly committed Mar 8, 2024
2 parents be34913 + 0360c16 commit b75cb0d
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 72 deletions.
33 changes: 33 additions & 0 deletions src/app/attendees/conference-selection.tsx
@@ -0,0 +1,33 @@
import { type FC } from "react"

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { type ConferencePreview } from "@/contracts/conference/conference"
import { preventDefaultOnReference } from "@/lib/events/prevent-default-on-reference"

export type ConferenceSelectionProps = {
conferences: ConferencePreview[]
onSelect: (conferenceId: string) => void
}

export const ConferenceSelection: FC<ConferenceSelectionProps> = (props) => {
return (
<Select onValueChange={props.onSelect}>
<SelectTrigger>
<SelectValue placeholder={"Conferences you have tickets for"} />
</SelectTrigger>
<SelectContent ref={preventDefaultOnReference}>
{props.conferences.map((conference) => (
<SelectItem key={conference.id} value={conference.id}>
{conference.name}
</SelectItem>
))}
</SelectContent>
</Select>
)
}
38 changes: 38 additions & 0 deletions src/app/attendees/page.tsx
@@ -0,0 +1,38 @@
"use client"

import { useState } from "react"

import { ConferenceSelection } from "@/app/attendees/conference-selection"
import { ProfileList } from "@/app/attendees/profile-list.component"
import { NoticeText } from "@/components/ui/messages/notice-text"
import { PageTitle } from "@/components/ui/page-title"
import { api } from "@/trpc/react"

export default function AttendeesPage() {
const apiFetchAttendingConferences = api.attendance.myConferences.useQuery()

const [selectedConferenceId, setSelectedConferenceId] = useState<string>("")

return (
<div>
<PageTitle
title={"Attendees"}
subtitle={
"A list of all the people who have tickets for the selected conference"
}
/>
<div className={"mb-5"}>
<ConferenceSelection
conferences={apiFetchAttendingConferences.data ?? []}
onSelect={setSelectedConferenceId}
/>
</div>

{selectedConferenceId ? (
<ProfileList conferenceId={selectedConferenceId} />
) : (
<NoticeText text={"Select a conference to view attendees"} />
)}
</div>
)
}
30 changes: 30 additions & 0 deletions src/app/attendees/profile-list-item.component.tsx
@@ -0,0 +1,30 @@
import Link from "next/link"
import React, { type FC } from "react"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { type UserProfile } from "@/contracts/profile/user-profile"

export type ProfileListItemProps = {
profile: UserProfile
}

export const ProfileListItem: FC<ProfileListItemProps> = ({ profile }) => {
return (
<Link href={`/profiles/${profile.id}`}>
<li
key={profile.id}
className={"flex items-center space-x-3 border-b border-accent py-3"}>
<Avatar className={"h-12 w-12 flex-grow-0 rounded-full"}>
<AvatarImage src={profile.avatarUrl} alt={profile.displayName} />
<AvatarFallback className={"rounded"}>
{profile.initials}
</AvatarFallback>
</Avatar>
<div className={"flex-1"}>
<p className={"font-bold"}>{profile.displayName}</p>
<p className={"font-thin"}>{profile.title || "No Title"}</p>
</div>
</li>
</Link>
)
}
@@ -1,10 +1,8 @@
"use client"
import React, { type FC, useMemo } from "react"

import React, { useMemo } from "react"

import { ProfileList } from "@/app/profiles/profile-list.component"
import { ProfileListItem } from "@/app/attendees/profile-list-item.component"
import { SkeletonList } from "@/components/molecules/skeletons/skeleton-list"
import { PageTitle } from "@/components/ui/page-title"
import { MissingText } from "@/components/ui/messages/missing-text"
import {
Pagination,
PaginationContent,
Expand All @@ -16,15 +14,24 @@ import { api } from "@/trpc/react"

const PAGE_SIZE = 8

export default function ProfilePage() {
export type ProfileListProps = {
conferenceId: string
}

export const ProfileList: FC<ProfileListProps> = ({ conferenceId }) => {
const pager = usePagination()

const profileCountRequest = api.profile.count.useQuery()

const profilesRequest = api.profile.page.useQuery({
page: pager.page,
pageSize: PAGE_SIZE,
})
const profilesRequest = api.attendance.page.useQuery(
{
page: pager.page,
pageSize: PAGE_SIZE,
conferenceId,
},
{
enabled: !!conferenceId,
},
)

const pageNumbers = useMemo(() => {
if (!profileCountRequest.data) {
Expand All @@ -40,22 +47,30 @@ export default function ProfilePage() {
return [...previousPages, pager.page, ...nextPages]
}, [profileCountRequest.data, pager.page])

const profiles = useMemo(
() => profilesRequest.data?.items ?? [],
[profilesRequest.data],
)

// todo: encapsulate this into a trpc fetching component
if (profilesRequest.isLoading) {
return <SkeletonList count={PAGE_SIZE} />
}
if (profilesRequest.error) {
return <p>Failed to load profiles</p>
}

if (profiles.length < 1) {
return <MissingText text={"No profiles found"} />
}

return (
<div>
<PageTitle
title={"Participants"}
subtitle={
"A list of all the people who are partaking in this conference"
}
/>
{
// todo: create a trpc loading component (with built in error displaying e.t.c)
profilesRequest.isLoading ? (
<SkeletonList count={PAGE_SIZE} />
) : (
<ProfileList profiles={profilesRequest.data?.items ?? []} />
)
}
<ul className={"space-y-2"}>
{profiles.map((profile) => {
return <ProfileListItem key={profile.id} profile={profile} />
})}
</ul>
<div
className={
"absolute bottom-6 left-4 right-4 rounded-full bg-background"
Expand Down
46 changes: 0 additions & 46 deletions src/app/profiles/profile-list.component.tsx

This file was deleted.

16 changes: 15 additions & 1 deletion src/components/sidebar.tsx
@@ -1,4 +1,11 @@
import { BookmarkCheck, BookOpen, Home, Ticket, User } from "lucide-react"
import {
BookmarkCheck,
BookOpen,
Home,
Ticket,
User,
Users,
} from "lucide-react"
import Image from "next/image"
import Link from "next/link"

Expand Down Expand Up @@ -50,6 +57,13 @@ const Sidebar = ({ isSidebarOpen, setIsSidebarOpen }: SidebarProps) => {
superuserRequired: false,
icon: Ticket,
},
{
href: "/attendees",
title: "Attendees",
label: "",
superuserRequired: false,
icon: Users,
},
{
href: "/conferences",
title: "Conferences",
Expand Down
19 changes: 19 additions & 0 deletions src/components/ui/messages/notice-text.tsx
@@ -0,0 +1,19 @@
import { cn } from "@/lib/utils"

export type MissingTextProps = {
text: string
className?: string
}

/**
* Represents a component for displaying a default missing text with a sad emoji.
* @param text - The text to display.
* @param className - An optional class name to apply to the component.
*/
export const NoticeText = ({ text, className }: MissingTextProps) => {
return (
<div className={cn("text-center font-light", className)}>
<p>{text}</p>
</div>
)
}
7 changes: 7 additions & 0 deletions src/contracts/conference/conference.ts
Expand Up @@ -14,6 +14,11 @@ export const ConferenceProfileDto = z.object({
stripeId: z.string(),
})

export const ConferencePreviewDto = ConferenceProfileDto.pick({
id: true,
name: true,
})

export const CreateConferenceInputDto = ConferenceProfileDto.pick({
name: true,
description: true,
Expand All @@ -34,6 +39,8 @@ export const UpdateConferenceInputDto = ConferenceProfileDto.partial()
// Types
export type ConferenceProfile = z.infer<typeof ConferenceProfileDto>

export type ConferencePreview = z.infer<typeof ConferencePreviewDto>

export type CreateConferenceInput = z.infer<typeof CreateConferenceInputDto>

export type UpdateConferenceInput = z.infer<typeof UpdateConferenceInputDto>
21 changes: 21 additions & 0 deletions src/lib/events/prevent-default-on-reference.ts
@@ -0,0 +1,21 @@
/**
* Prevents default behavior on touchend and click events for the given HTML element reference.
*
* @param ref - The HTML element reference on which to prevent default behavior.
* @returns The modified HTML element reference with default behavior prevention on touchend and click events.
* @link https://github.com/radix-ui/primitives/issues/1658#issuecomment-1690666012
*/
export const preventDefaultOnReference = (ref: HTMLElement | null) => {
if (!ref) {
return
}

ref.ontouchend = (e) => {
e.preventDefault()
}
ref.onclick = (e) => {
e.preventDefault()
}

return ref
}
12 changes: 12 additions & 0 deletions src/lib/events/prevent-default.ts
@@ -0,0 +1,12 @@
import { type FormEvent, type TouchEvent } from "react"

/**
* A higher-order function that prevents the default behavior of the event and then calls the provided method.
* @param method - The method to call after preventing default behavior.
* @returns A function that takes an event and prevents its default behavior before calling the method.
*/
export const preventDefault =
(method: () => void) => (e: FormEvent | TouchEvent | MouseEvent) => {
e.preventDefault()
method()
}
12 changes: 12 additions & 0 deletions src/lib/events/stop-propagation.ts
@@ -0,0 +1,12 @@
import { type MouseEvent } from "react"

/**
* Stops the propagation of the event and executes the provided method.
* @param method - The method to execute.
* @returns A function that stops event propagation and executes the provided method.
*/
export const stopPropagation =
(method: () => void) => (e: MouseEvent | TouchEvent) => {
e.stopPropagation()
method()
}
2 changes: 2 additions & 0 deletions src/server/api/root.ts
@@ -1,3 +1,4 @@
import { attendanceRouter } from "@/server/api/routers/attendance/attendance.router"
import { companyRouter } from "@/server/api/routers/company/company.router"
import { conferenceRouter } from "@/server/api/routers/conference/conference.router"
import { profileRouter } from "@/server/api/routers/profile/profile.router"
Expand All @@ -16,6 +17,7 @@ export const appRouter = createTRPCRouter({
user: userRouter,
profile: profileRouter,
ticket: ticketRouter,
attendance: attendanceRouter,
})

// export type definition of API
Expand Down
@@ -0,0 +1,24 @@
import { desc, eq } from "drizzle-orm"

import { type ConferencePreview } from "@/contracts/conference/conference"
import { db } from "@/database"
import { conferences, tickets } from "@/database/schemas"
import { protectedProcedure } from "@/server/api/trpc"

export const attendanceMyConferencesProcedure = protectedProcedure.query(
async ({ ctx }): Promise<ConferencePreview[]> => {
const userId = ctx.user.id

const conferencesUserIsAttending = await db
.selectDistinct({ conference: conferences })
.from(conferences)
.leftJoin(tickets, eq(conferences.id, tickets.conferenceId))
.where(eq(tickets.userId, userId))
.orderBy(desc(conferences.startDate))

return conferencesUserIsAttending.map(({ conference }) => ({
id: conference.id,
name: conference.name,
}))
},
)

0 comments on commit b75cb0d

Please sign in to comment.