diff --git a/package.json b/package.json
index cc03954..f36e818 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"lucide-react": "^1.14.0",
"nanoid": "^5.1.11",
"next-themes": "^0.4.6",
+ "qrcode.react": "^4.2.0",
"radix-ui": "^1.4.3",
"react": "^19.2.5",
"react-day-picker": "^9.14.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4218d5a..ff838c9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ qrcode.react:
+ specifier: ^4.2.0
+ version: 4.2.0(react@19.2.5)
radix-ui:
specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -2829,6 +2832,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ qrcode.react@4.2.0:
+ resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
qs@6.15.1:
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
engines: {node: '>=0.6'}
@@ -5991,6 +5999,10 @@ snapshots:
punycode@2.3.1: {}
+ qrcode.react@4.2.0(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+
qs@6.15.1:
dependencies:
side-channel: 1.1.0
diff --git a/src/components/generic/OverflowBadges.jsx b/src/components/generic/OverflowBadges.jsx
new file mode 100644
index 0000000..09ac184
--- /dev/null
+++ b/src/components/generic/OverflowBadges.jsx
@@ -0,0 +1,124 @@
+/***********************************************************************************************************************************************************************
+ * File Name: OverflowBadges.jsx
+ * Type of Program: Generic Component
+ * Description: Renders up to `max` badges inline. Remaining items collapse into a
+ * "+N" overflow button that opens a dialog listing all items.
+ *
+ * HOW TO USE:
+ *
+ *
+ * // Primitive arrays (no keys needed):
+ *
+ ***********************************************************************************************************************************************************************/
+import { useState } from "react";
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+
+/**
+ * @param {object} props
+ * @param {Array} props.items Array of objects or primitives to render.
+ * @param {string} [props.labelKey] Key on each item to use as the badge label. Omit for primitive arrays.
+ * @param {string} [props.dialogTitleKey] Key on each item to show as the row title in the dialog. Falls back to labelKey.
+ * @param {string} [props.keyKey] Key on each item to use as the React key. Falls back to index.
+ * @param {string} [props.dialogTitle] Heading shown in the overflow dialog. Default: "All items".
+ * @param {number} [props.max] Max badges shown inline before collapsing. Default: 2.
+ * @param {string} [props.badgeVariant] shadcn Badge variant. Default: "outline".
+ * @param {string} [props.badgeClassName] Extra className on each Badge.
+ * @param {string} [props.emptyText] Text shown when items is empty. Default: "—".
+ */
+export function OverflowBadges({
+ items = [],
+ labelKey,
+ dialogTitleKey,
+ keyKey,
+ dialogTitle = "All items",
+ max = 1,
+ badgeVariant = "outline",
+ badgeClassName = "text-xs",
+ emptyText = "—",
+}) {
+ const [open, setOpen] = useState(false);
+
+ if (!items.length)
+ return {emptyText};
+
+ const getLabel = (item) =>
+ labelKey ? item[labelKey] : String(item);
+
+ const getTitle = (item) =>
+ dialogTitleKey ? item[dialogTitleKey] : labelKey ? item[labelKey] : String(item);
+
+ const getKey = (item, i) =>
+ keyKey ? item[keyKey] : i;
+
+ const visible = items.slice(0, max);
+ const overflow = items.slice(max);
+
+ return (
+ <>
+
+ {visible.map((item, i) => (
+
+ {getLabel(item)}
+
+ ))}
+
+ {overflow.length > 0 && (
+
+ )}
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/contexts/AdminUserGroupContext.jsx b/src/contexts/AdminUserGroupContext.jsx
index 95dc4e0..8919f9b 100644
--- a/src/contexts/AdminUserGroupContext.jsx
+++ b/src/contexts/AdminUserGroupContext.jsx
@@ -117,9 +117,9 @@ export function UserGroupProvider({ children }) {
// ─── POST /api/admin/groups ────────────────────────────────────────────────
const createGroup = useCallback(
- ({ name, description }) =>
+ ({ name, description, group_code }) =>
request(async () => {
- const res = await api.post(`${BASE}/groups`, { name, description });
+ const res = await api.post(`${BASE}/groups`, { name, description, group_code });
setGroups((prev) => [res.data?.data, ...prev]);
toast.success("Group created successfully.");
return res.data;
@@ -129,9 +129,9 @@ export function UserGroupProvider({ children }) {
// ─── PUT /api/admin/groups/:gid ───────────────────────────────────────────
const updateGroup = useCallback(
- (gid, { name, description }) =>
+ (gid, { name, description, group_code }) =>
request(async () => {
- const res = await api.put(`${BASE}/groups/${gid}`, { name, description });
+ const res = await api.put(`${BASE}/groups/${gid}`, { name, description, group_code });
setGroups((prev) =>
prev.map((g) => (g.group_id === gid ? { ...g, ...res.data?.data } : g))
);
diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx
index 6073426..6ab1afc 100644
--- a/src/contexts/AuthContext.jsx
+++ b/src/contexts/AuthContext.jsx
@@ -7,7 +7,7 @@ const AuthContext = createContext(null)
export const decodeToken = (token) => JSON.parse(atob(token.split('.')[1]))
export function AuthProvider({ children }) {
- const [accessToken, _setAccessToken] = useState(null) // ← renamed to _setAccessToken
+ const [accessToken, _setAccessToken] = useState(null)
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [sessionRestored, setSessionRestored] = useState(false)
@@ -16,12 +16,12 @@ export function AuthProvider({ children }) {
const isRestoring = useRef(false)
const accessTokenRef = useRef(null)
- // ← Single setter that updates both ref and state
const setAccessToken = useCallback((token) => {
accessTokenRef.current = token
_setAccessToken(token)
}, [])
+ // ── Login ──────────────────────────────────────────────────────────────────
const login = useCallback(async ({ email, password }) => {
setAuthError(null)
try {
@@ -36,6 +36,46 @@ export function AuthProvider({ children }) {
}
}, [])
+ // ── Register ───────────────────────────────────────────────────────────────
+ const register = useCallback(async (payload) => {
+ setAuthError(null)
+ try {
+ await api.post('/auth/register', payload)
+ return { success: true }
+ } catch (err) {
+ const message = err.response?.data?.message || 'Registration failed. Please try again.'
+ setAuthError(message)
+ return { success: false, message }
+ }
+ }, [])
+
+ // ── Verify OTP (auto-login) ────────────────────────────────────────────────
+ const verifyOTP = useCallback(async ({ email, otp }) => {
+ setAuthError(null)
+ try {
+ const { data } = await api.post('/auth/verify-otp', { email, otp })
+ setAccessToken(data.data.accessToken)
+ setUser(data.data.user)
+ return { success: true, user: data.data.user }
+ } catch (err) {
+ const message = err.response?.data?.message || 'OTP verification failed.'
+ setAuthError(message)
+ return { success: false, message }
+ }
+ }, [])
+
+ // ── Resend OTP ─────────────────────────────────────────────────────────────
+ const resendOTP = useCallback(async ({ email }) => {
+ try {
+ await api.post('/auth/resend-otp', { email })
+ return { success: true }
+ } catch (err) {
+ const message = err.response?.data?.message || 'Could not resend OTP.'
+ return { success: false, message }
+ }
+ }, [])
+
+ // ── Logout ─────────────────────────────────────────────────────────────────
const logout = useCallback(async () => {
try {
await api.post('/auth/logout')
@@ -47,12 +87,13 @@ export function AuthProvider({ children }) {
}
}, [])
+ // ── Restore session ────────────────────────────────────────────────────────
const restoreSession = useCallback(async () => {
if (isRestoring.current) return
isRestoring.current = true
try {
- if (accessTokenRef.current) return // already have token, skip
+ if (accessTokenRef.current) return
const { data } = await api.post('/auth/refresh')
setAccessToken(data.data.accessToken)
setUser(data.data.user)
@@ -60,18 +101,21 @@ export function AuthProvider({ children }) {
setAccessToken(null)
setUser(null)
} finally {
- setLoading(false) // ← set once, never back to true
+ setLoading(false)
}
}, [])
return (
@@ -90,13 +112,24 @@ export function AddGroupDialog({ open, onOpenChange, onSubmit, loading }) {
)}
+
+
+
+ {errors.group_code && (
+
{errors.group_code.message}
+ )}
+
+
-