Skip to content

Commit 5eface4

Browse files
authored
Redeemablebugfixes (#297)
* Everything except prettifying the overview page * Made overview page a little more paletable * Include full id * Checkin check before scanning qr code * Update sqlc
1 parent a10695e commit 5eface4

File tree

13 files changed

+377
-37
lines changed

13 files changed

+377
-37
lines changed

apps/api/internal/api/api.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
152152
r.With(ensureEventStaff).Get("/users/{userId}", api.Handlers.Event.GetUserForEvent)
153153
// Get user ID by RFID
154154
r.With(ensureEventStaff).Get("/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID)
155+
// Is the user checked in
156+
r.With(ensureEventStaff).Get("/users/{userId}/checked-in-status", api.Handlers.Event.GetCheckedInStatusByIds)
155157

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

230232
r.Route("/users/{userId}", func(r chi.Router) {
231233
r.Post("/", api.Handlers.Redeemables.RedeemRedeemable)

apps/api/internal/api/handlers/events.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"strconv"
910
"time"
1011

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

866867
// Return just the user ID as a simple JSON object
867868
res.Send(w, http.StatusOK, map[string]string{"user_id": user.ID.String()})
868-
}
869+
}
870+
871+
// Get User by RFID
872+
//
873+
// @Summary Retrieves a user's ID by their RFID
874+
// @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.
875+
// @Tags Event
876+
// @Produce json
877+
// @Param eventId path string true "Event ID" Format(uuid)
878+
// @Param rfid path string true "RFID code (10 digits)"
879+
// @Success 200 {object} map[string]string "OK - Returns user ID"
880+
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
881+
// @Failure 404 {object} response.ErrorResponse "User not found with the provided RFID"
882+
// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user by RFID"
883+
// @Router /events/{eventId}/users/by-rfid/{rfid} [get]
884+
func (h *EventHandler) GetCheckedInStatusByIds(w http.ResponseWriter, r *http.Request) {
885+
eventId, err := web.PathParamToUUID(r, "eventId")
886+
if err != nil {
887+
res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!"))
888+
return
889+
}
890+
891+
userId, err := web.PathParamToUUID(r, "userId")
892+
if err != nil {
893+
res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!"))
894+
return
895+
}
896+
result, err := h.eventService.GetCheckedInStatusByIds(r.Context(), userId, eventId)
897+
if err != nil {
898+
res.SendError(w, http.StatusNotFound, res.NewError("error", "Something went wrong internally."))
899+
return
900+
}
901+
// Return just the checked in status as a simple JSON object
902+
res.Send(w, http.StatusOK, map[string]string{
903+
"checked_in_status": strconv.FormatBool(result),
904+
})
905+
}

apps/api/internal/db/queries/event_roles.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,12 @@ FROM auth.users u
7171
JOIN event_roles er ON u.id = er.user_id
7272
WHERE er.event_id = $1
7373
AND er.rfid = $2;
74+
75+
-- name: GetCheckedInStatusByIds :one
76+
SELECT EXISTS (
77+
SELECT 1
78+
FROM event_roles
79+
WHERE user_id = $1
80+
AND event_id = $2
81+
AND checked_in_at IS NOT NULL
82+
)::bool;

apps/api/internal/db/repository/events.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var (
1616
ErrDuplicateEvent = errors.New("event already exists in database")
1717
ErrNoEventsDeleted = errors.New("no events deleted")
1818
ErrMultipleEventsDeleted = errors.New("multiple events affected by delete query while only expecting one to delete one")
19+
ErrUserEventNotFound = errors.New("the user and event id combination was not found")
1920
ErrUnknown = errors.New("an unkown error was caught")
2021
)
2122

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

210+
func (r *EventRepository) GetCheckedInStatusByUserIdAndEventId(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) {
211+
params := sqlc.GetCheckedInStatusByIdsParams{
212+
UserID: userId,
213+
EventID: eventId,
214+
}
215+
216+
result, err := r.db.Query.GetCheckedInStatusByIds(ctx, params)
217+
218+
if err != nil {
219+
return false, err
220+
}
221+
222+
return result, nil
223+
209224
func (r *EventRepository) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) {
210225
return r.db.Query.GetAttendeeUserIdsByEventId(ctx, eventID)
211226
}

apps/api/internal/db/sqlc/event_roles.sql.go

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/api/internal/services/events.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,13 @@ func (s *EventService) GetUserInfoForEvent(ctx context.Context, userId, eventId
422422

423423
return result, nil
424424
}
425+
426+
func (s *EventService) GetCheckedInStatusByIds(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) {
427+
result, err := s.eventRepo.GetCheckedInStatusByUserIdAndEventId(ctx, userId, eventId)
428+
if err != nil {
429+
s.logger.Err(err).Msg("Failed to get user by ids")
430+
return false, err
431+
}
432+
return result, nil
433+
434+
}

apps/api/internal/services/redeemables.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ func (s *RedeemablesService) UpdateRedeemable(ctx context.Context, redeemableID
6060
}
6161

6262
func (s *RedeemablesService) RedeemRedeemable(ctx context.Context, redeemableID uuid.UUID, userID uuid.UUID) error {
63+
// Need to do a check to see if the user is checked in
64+
// Probably need event service
65+
66+
// CREATE NEW SQL function for getting checked in status
6367
_, err := s.redeemablesRepo.RedeemRedeemable(ctx, redeemableID, userID)
6468

6569
if err != nil {

apps/web/src/features/EventOverview/components/AttendeeOverview.tsx

Lines changed: 153 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Button } from "@/components/ui/Button";
22
import { Card } from "@/components/ui/Card";
33
import { EventAttendanceWithdrawalModal } from "@/features/Event/components/EventAttendanceWithdrawalModal";
44
import { generateIdentifyIntent } from "@/lib/qr-intents/generate";
5-
import { DialogTrigger, Heading } from "react-aria-components";
5+
import { DialogTrigger, Heading, Link } from "react-aria-components";
66
import QRCode from "react-qr-code";
77

88
interface Props {
@@ -12,35 +12,164 @@ interface Props {
1212

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

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

2119
return (
22-
<main>
23-
<Heading className="text-2xl lg:text-3xl font-semibold mb-6">
20+
<main className="max-w-5xl p-4 sm:p-6">
21+
<Heading className="text-2xl lg:text-3xl font-semibold mb-8 text-slate-900 dark:text-slate-50">
2422
Overview
2523
</Heading>
2624

27-
<Card className="p-5 w-full md:w-64">
28-
<QRCode
29-
bgColor={bg}
30-
fgColor={fg}
31-
className="h-full w-full"
32-
value={identificationIntentString}
33-
/>
34-
</Card>
35-
36-
<div>
37-
<p className="my-5">Can't make it to the event?</p>
38-
<DialogTrigger>
39-
<Button variant="danger">{"Withdraw Attendance"}</Button>
40-
<EventAttendanceWithdrawalModal
41-
eventId={eventId}
42-
></EventAttendanceWithdrawalModal>
43-
</DialogTrigger>
25+
<div className="flex flex-col lg:flex-row gap-8">
26+
{/* Left Column: QR ID Card */}
27+
<div className="w-full lg:w-72 shrink-0">
28+
<Card className="relative overflow-hidden border-none bg-gradient-to-br from-blue-600 to-indigo-700 p-1 shadow-xl">
29+
<div className="bg-white dark:bg-slate-900 rounded-[calc(var(--radius)-4px)] p-6">
30+
<div className="flex flex-col items-center">
31+
<div className="mb-4 text-center">
32+
<span className="text-[10px] uppercase tracking-widest font-bold text-slate-400 dark:text-slate-500">
33+
Attendee Pass
34+
</span>
35+
</div>
36+
37+
{/* HIGH CONTRAST QR WRAPPER */}
38+
{/* We force bg-white and text-black here regardless of theme for scan reliability */}
39+
<div className="p-4 bg-white rounded-2xl shadow-sm ring-1 ring-slate-100">
40+
<QRCode
41+
size={200}
42+
style={{ height: "auto", maxWidth: "100%", width: "100%" }}
43+
value={identificationIntentString}
44+
viewBox={`0 0 256 256`}
45+
bgColor="#FFFFFF"
46+
fgColor="#000000"
47+
level="H" // High error correction for better scanning
48+
/>
49+
</div>
50+
51+
<div className="mt-6 text-center">
52+
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
53+
Personal QR Code
54+
</p>
55+
<p className="font-mono text-[10px] text-slate-500 dark:text-slate-400 mt-1 uppercase">
56+
ID: {userId}
57+
</p>
58+
</div>
59+
</div>
60+
</div>
61+
</Card>
62+
</div>
63+
64+
{/* Right Column: Info & Links */}
65+
<div className="flex-1 flex flex-col gap-8">
66+
{/* QR Explanation */}
67+
<section className="space-y-3">
68+
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-50">
69+
Your Identifier
70+
</h3>
71+
<div className="prose prose-slate dark:prose-invert">
72+
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
73+
This QR code is your unique digital badge. It identifies you to
74+
our staff and is used for check-ins, swag pickups, and meal
75+
redemptions!
76+
</p>
77+
</div>
78+
</section>
79+
80+
<hr className="border-slate-200 dark:border-slate-800" />
81+
82+
{/* Unified Style Resource Links */}
83+
<section className="grid gap-4">
84+
<Link
85+
href={hackerGuideUrl}
86+
target="_blank"
87+
rel="noopener noreferrer"
88+
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"
89+
>
90+
<div className="flex items-center gap-5">
91+
{/* Consistent Icon Style */}
92+
<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">
93+
<svg
94+
width="24"
95+
height="24"
96+
viewBox="0 0 24 24"
97+
fill="none"
98+
stroke="currentColor"
99+
strokeWidth="2"
100+
strokeLinecap="round"
101+
strokeLinejoin="round"
102+
>
103+
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
104+
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
105+
</svg>
106+
</div>
107+
108+
<div>
109+
<h4 className="font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
110+
Official Hacker Guide
111+
<svg
112+
className="text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity"
113+
width="14"
114+
height="14"
115+
viewBox="0 0 24 24"
116+
fill="none"
117+
stroke="currentColor"
118+
strokeWidth="2.5"
119+
strokeLinecap="round"
120+
strokeLinejoin="round"
121+
>
122+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
123+
<polyline points="15 3 21 3 21 9"></polyline>
124+
<line x1="10" y1="14" x2="21" y2="3"></line>
125+
</svg>
126+
</h4>
127+
<p className="text-sm text-slate-500 dark:text-slate-400">
128+
Have questions? Look here!
129+
</p>
130+
</div>
131+
</div>
132+
133+
<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">
134+
Open Guide
135+
<svg
136+
width="14"
137+
height="14"
138+
viewBox="0 0 24 24"
139+
fill="none"
140+
stroke="currentColor"
141+
strokeWidth="2.5"
142+
strokeLinecap="round"
143+
strokeLinejoin="round"
144+
>
145+
<polyline points="9 18 15 12 9 6"></polyline>
146+
</svg>
147+
</div>
148+
</Link>
149+
</section>
150+
151+
{/* Management / Withdrawal */}
152+
{allowWithdrawal && (
153+
<div className="mt-auto pt-6 border-t border-slate-200 dark:border-slate-800">
154+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
155+
<div>
156+
<p className="font-medium text-slate-900 dark:text-slate-100">
157+
Plans changed?
158+
</p>
159+
<p className="text-sm text-slate-500 dark:text-slate-400">
160+
Withdrawal is permanent for this event.
161+
</p>
162+
</div>
163+
<DialogTrigger>
164+
<Button variant="danger" className="shrink-0">
165+
Withdraw Attendance
166+
</Button>
167+
<EventAttendanceWithdrawalModal eventId={eventId} />
168+
</DialogTrigger>
169+
</div>
170+
</div>
171+
)}
172+
</div>
44173
</div>
45174
</main>
46175
);

apps/web/src/features/Redeemables/components/RedeemableCard.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface RedeemableCardProps {
99
maxUserAmount: number;
1010
totalRedeemed: number;
1111
eventId: string;
12+
eventRole: string | undefined;
1213
}
1314

1415
export function RedeemableCard({
@@ -18,6 +19,7 @@ export function RedeemableCard({
1819
maxUserAmount,
1920
totalRedeemed,
2021
eventId,
22+
eventRole,
2123
}: RedeemableCardProps) {
2224
const remaining = totalStock - (totalRedeemed as number);
2325
const percentageRemaining =
@@ -63,6 +65,7 @@ export function RedeemableCard({
6365
maxUserAmount={maxUserAmount}
6466
totalRedeemed={totalRedeemed as number}
6567
eventId={eventId}
68+
eventRole={eventRole}
6669
/>
6770
</DialogTrigger>
6871
</div>

0 commit comments

Comments
 (0)