Skip to content
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
4 changes: 3 additions & 1 deletion apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
r.With(ensureEventStaff).Get("/users/{userId}", api.Handlers.Event.GetUserForEvent)
// Get user ID by RFID
r.With(ensureEventStaff).Get("/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID)
// Is the user checked in
r.With(ensureEventStaff).Get("/users/{userId}/checked-in-status", api.Handlers.Event.GetCheckedInStatusByIds)

// Admin-only
r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail)
Expand Down Expand Up @@ -225,7 +227,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
// Update and delete specific redeemable
r.Route("/{redeemableId}", func(r chi.Router) {
r.Patch("/", api.Handlers.Redeemables.UpdateRedeemable)
r.Delete("/", api.Handlers.Redeemables.DeleteRedeemable)
r.With(ensureEventAdmin).Delete("/", api.Handlers.Redeemables.DeleteRedeemable)

r.Route("/users/{userId}", func(r chi.Router) {
r.Post("/", api.Handlers.Redeemables.RedeemRedeemable)
Expand Down
39 changes: 38 additions & 1 deletion apps/api/internal/api/handlers/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"time"

"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -865,4 +866,40 @@ func (h *EventHandler) GetUserByRFID(w http.ResponseWriter, r *http.Request) {

// Return just the user ID as a simple JSON object
res.Send(w, http.StatusOK, map[string]string{"user_id": user.ID.String()})
}
}

// Get User by RFID
//
// @Summary Retrieves a user's ID by their RFID
// @Description Looks up a user's ID by their RFID code for a specific event. Returns the user ID which can be used for other operations.
// @Tags Event
// @Produce json
// @Param eventId path string true "Event ID" Format(uuid)
// @Param rfid path string true "RFID code (10 digits)"
// @Success 200 {object} map[string]string "OK - Returns user ID"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 404 {object} response.ErrorResponse "User not found with the provided RFID"
// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user by RFID"
// @Router /events/{eventId}/users/by-rfid/{rfid} [get]
func (h *EventHandler) GetCheckedInStatusByIds(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!"))
return
}

userId, err := web.PathParamToUUID(r, "userId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!"))
return
}
result, err := h.eventService.GetCheckedInStatusByIds(r.Context(), userId, eventId)
if err != nil {
res.SendError(w, http.StatusNotFound, res.NewError("error", "Something went wrong internally."))
return
}
// Return just the checked in status as a simple JSON object
res.Send(w, http.StatusOK, map[string]string{
"checked_in_status": strconv.FormatBool(result),
})
}
9 changes: 9 additions & 0 deletions apps/api/internal/db/queries/event_roles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,12 @@ FROM auth.users u
JOIN event_roles er ON u.id = er.user_id
WHERE er.event_id = $1
AND er.rfid = $2;

-- name: GetCheckedInStatusByIds :one
SELECT EXISTS (
SELECT 1
FROM event_roles
WHERE user_id = $1
AND event_id = $2
AND checked_in_at IS NOT NULL
)::bool;
15 changes: 15 additions & 0 deletions apps/api/internal/db/repository/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
ErrDuplicateEvent = errors.New("event already exists in database")
ErrNoEventsDeleted = errors.New("no events deleted")
ErrMultipleEventsDeleted = errors.New("multiple events affected by delete query while only expecting one to delete one")
ErrUserEventNotFound = errors.New("the user and event id combination was not found")
ErrUnknown = errors.New("an unkown error was caught")
)

Expand Down Expand Up @@ -206,6 +207,20 @@ func (r *EventRepository) GetEventRoleByDiscordIDAndEventId(ctx context.Context,
return &eventRole, nil
}

func (r *EventRepository) GetCheckedInStatusByUserIdAndEventId(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) {
params := sqlc.GetCheckedInStatusByIdsParams{
UserID: userId,
EventID: eventId,
}

result, err := r.db.Query.GetCheckedInStatusByIds(ctx, params)

if err != nil {
return false, err
}

return result, nil

func (r *EventRepository) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) {
return r.db.Query.GetAttendeeUserIdsByEventId(ctx, eventID)
}
22 changes: 22 additions & 0 deletions apps/api/internal/db/sqlc/event_roles.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions apps/api/internal/services/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,13 @@ func (s *EventService) GetUserInfoForEvent(ctx context.Context, userId, eventId

return result, nil
}

func (s *EventService) GetCheckedInStatusByIds(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) {
result, err := s.eventRepo.GetCheckedInStatusByUserIdAndEventId(ctx, userId, eventId)
if err != nil {
s.logger.Err(err).Msg("Failed to get user by ids")
return false, err
}
return result, nil

}
4 changes: 4 additions & 0 deletions apps/api/internal/services/redeemables.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (s *RedeemablesService) UpdateRedeemable(ctx context.Context, redeemableID
}

func (s *RedeemablesService) RedeemRedeemable(ctx context.Context, redeemableID uuid.UUID, userID uuid.UUID) error {
// Need to do a check to see if the user is checked in
// Probably need event service

// CREATE NEW SQL function for getting checked in status
_, err := s.redeemablesRepo.RedeemRedeemable(ctx, redeemableID, userID)

if err != nil {
Expand Down
177 changes: 153 additions & 24 deletions apps/web/src/features/EventOverview/components/AttendeeOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { EventAttendanceWithdrawalModal } from "@/features/Event/components/EventAttendanceWithdrawalModal";
import { generateIdentifyIntent } from "@/lib/qr-intents/generate";
import { DialogTrigger, Heading } from "react-aria-components";
import { DialogTrigger, Heading, Link } from "react-aria-components";
import QRCode from "react-qr-code";

interface Props {
Expand All @@ -12,35 +12,164 @@ interface Props {

export default function AttendeeOverview({ userId, eventId }: Props) {
const identificationIntentString = generateIdentifyIntent(userId);
const hackerGuideUrl = "https://swamphack.notion.site/sh-xi-hacker-guide";

// Used to get the right colors for QR Code
const styles = getComputedStyle(document.documentElement);
const bg = styles.getPropertyValue("--surface").trim();
const fg = styles.getPropertyValue("--text-main").trim();
const allowWithdrawal = false;

return (
<main>
<Heading className="text-2xl lg:text-3xl font-semibold mb-6">
<main className="max-w-5xl p-4 sm:p-6">
<Heading className="text-2xl lg:text-3xl font-semibold mb-8 text-slate-900 dark:text-slate-50">
Overview
</Heading>

<Card className="p-5 w-full md:w-64">
<QRCode
bgColor={bg}
fgColor={fg}
className="h-full w-full"
value={identificationIntentString}
/>
</Card>

<div>
<p className="my-5">Can't make it to the event?</p>
<DialogTrigger>
<Button variant="danger">{"Withdraw Attendance"}</Button>
<EventAttendanceWithdrawalModal
eventId={eventId}
></EventAttendanceWithdrawalModal>
</DialogTrigger>
<div className="flex flex-col lg:flex-row gap-8">
{/* Left Column: QR ID Card */}
<div className="w-full lg:w-72 shrink-0">
<Card className="relative overflow-hidden border-none bg-gradient-to-br from-blue-600 to-indigo-700 p-1 shadow-xl">
<div className="bg-white dark:bg-slate-900 rounded-[calc(var(--radius)-4px)] p-6">
<div className="flex flex-col items-center">
<div className="mb-4 text-center">
<span className="text-[10px] uppercase tracking-widest font-bold text-slate-400 dark:text-slate-500">
Attendee Pass
</span>
</div>

{/* HIGH CONTRAST QR WRAPPER */}
{/* We force bg-white and text-black here regardless of theme for scan reliability */}
<div className="p-4 bg-white rounded-2xl shadow-sm ring-1 ring-slate-100">
<QRCode
size={200}
style={{ height: "auto", maxWidth: "100%", width: "100%" }}
value={identificationIntentString}
viewBox={`0 0 256 256`}
bgColor="#FFFFFF"
fgColor="#000000"
level="H" // High error correction for better scanning
/>
</div>

<div className="mt-6 text-center">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
Personal QR Code
</p>
<p className="font-mono text-[10px] text-slate-500 dark:text-slate-400 mt-1 uppercase">
ID: {userId}
</p>
</div>
</div>
</div>
</Card>
</div>

{/* Right Column: Info & Links */}
<div className="flex-1 flex flex-col gap-8">
{/* QR Explanation */}
<section className="space-y-3">
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-50">
Your Identifier
</h3>
<div className="prose prose-slate dark:prose-invert">
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
This QR code is your unique digital badge. It identifies you to
our staff and is used for check-ins, swag pickups, and meal
redemptions!
</p>
</div>
</section>

<hr className="border-slate-200 dark:border-slate-800" />

{/* Unified Style Resource Links */}
<section className="grid gap-4">
<Link
href={hackerGuideUrl}
target="_blank"
rel="noopener noreferrer"
className="group relative flex items-center justify-between p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm transition-all hover:border-blue-500 dark:hover:border-blue-400 hover:shadow-md no-underline"
>
<div className="flex items-center gap-5">
{/* Consistent Icon Style */}
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 group-hover:bg-amber-600 group-hover:text-white transition-colors">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>
</div>

<div>
<h4 className="font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
Official Hacker Guide
<svg
className="text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</h4>
<p className="text-sm text-slate-500 dark:text-slate-400">
Have questions? Look here!
</p>
</div>
</div>

<div className="flex items-center gap-1.5 font-semibold text-xs text-blue-600 dark:text-blue-400 md:bg-blue-50 md:dark:bg-blue-900/20 md:px-4 md:py-2 md:rounded-full group-hover:bg-blue-100 dark:group-hover:bg-blue-900/40 transition-colors">
Open Guide
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</div>
</Link>
</section>

{/* Management / Withdrawal */}
{allowWithdrawal && (
<div className="mt-auto pt-6 border-t border-slate-200 dark:border-slate-800">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">
Plans changed?
</p>
<p className="text-sm text-slate-500 dark:text-slate-400">
Withdrawal is permanent for this event.
</p>
</div>
<DialogTrigger>
<Button variant="danger" className="shrink-0">
Withdraw Attendance
</Button>
<EventAttendanceWithdrawalModal eventId={eventId} />
</DialogTrigger>
</div>
</div>
)}
</div>
</div>
</main>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface RedeemableCardProps {
maxUserAmount: number;
totalRedeemed: number;
eventId: string;
eventRole: string | undefined;
}

export function RedeemableCard({
Expand All @@ -18,6 +19,7 @@ export function RedeemableCard({
maxUserAmount,
totalRedeemed,
eventId,
eventRole,
}: RedeemableCardProps) {
const remaining = totalStock - (totalRedeemed as number);
const percentageRemaining =
Expand Down Expand Up @@ -63,6 +65,7 @@ export function RedeemableCard({
maxUserAmount={maxUserAmount}
totalRedeemed={totalRedeemed as number}
eventId={eventId}
eventRole={eventRole}
/>
</DialogTrigger>
</div>
Expand Down
Loading