diff --git a/backend/api/settings.py b/backend/api/settings.py
index 5e3372e16..56105b46e 100644
--- a/backend/api/settings.py
+++ b/backend/api/settings.py
@@ -125,7 +125,7 @@ class ThrottleScopes:
ThrottleScopes.PASSWORD_RESET.key: "10/hour",
ThrottleScopes.EVENT_CREATION.key: "25/hour",
ThrottleScopes.AVAILABILITY_ADD.key: "50/hour",
- ThrottleScopes.CODE_CHECK.key: "50/hour",
+ ThrottleScopes.CODE_CHECK.key: "10/min",
},
}
@@ -195,7 +195,7 @@ class ThrottleScopes:
"class": "logging.handlers.RotatingFileHandler",
"filename": f"{LOG_DIR}/django.log",
"formatter": "verbose",
- "level": "DEBUG",
+ "level": "DEBUG" if DEBUG else "INFO",
"maxBytes": 1024 * 1024 * 5, # 5 MB
"backupCount": 5,
},
diff --git a/frontend/src/app/(event)/[event-code]/edit/loading.tsx b/frontend/src/app/(event)/[event-code]/edit/loading.tsx
index 122b9bbb2..6ce716f29 100644
--- a/frontend/src/app/(event)/[event-code]/edit/loading.tsx
+++ b/frontend/src/app/(event)/[event-code]/edit/loading.tsx
@@ -6,7 +6,7 @@ export default function Loading() {
@@ -15,11 +15,12 @@ export default function Loading() {
-
+
-
-
-
+
+
+
+
);
diff --git a/frontend/src/app/(event)/[event-code]/loading.tsx b/frontend/src/app/(event)/[event-code]/loading.tsx
index 9f8912bc4..835d0486e 100644
--- a/frontend/src/app/(event)/[event-code]/loading.tsx
+++ b/frontend/src/app/(event)/[event-code]/loading.tsx
@@ -8,16 +8,15 @@ export default function Loading() {
diff --git a/frontend/src/app/(event)/[event-code]/painting/loading.tsx b/frontend/src/app/(event)/[event-code]/painting/loading.tsx
index 28a1cea40..948551c5c 100644
--- a/frontend/src/app/(event)/[event-code]/painting/loading.tsx
+++ b/frontend/src/app/(event)/[event-code]/painting/loading.tsx
@@ -8,20 +8,21 @@ export default function Loading() {
);
diff --git a/frontend/src/app/dashboard/loading.tsx b/frontend/src/app/dashboard/loading.tsx
index a0abdf5b1..d71e5b2d9 100644
--- a/frontend/src/app/dashboard/loading.tsx
+++ b/frontend/src/app/dashboard/loading.tsx
@@ -8,7 +8,7 @@ export default function Loading() {
{/* 2. Skeleton for the Event Grid */}
@@ -17,7 +17,7 @@ export default function Loading() {
{[1, 2, 3].map((i) => (
))}
diff --git a/frontend/src/app/settings/layout.tsx b/frontend/src/app/settings/(submenus)/layout.tsx
similarity index 100%
rename from frontend/src/app/settings/layout.tsx
rename to frontend/src/app/settings/(submenus)/layout.tsx
diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/(submenus)/page.tsx
similarity index 100%
rename from frontend/src/app/settings/page.tsx
rename to frontend/src/app/settings/(submenus)/page.tsx
diff --git a/frontend/src/app/settings/remove/page.tsx b/frontend/src/app/settings/(submenus)/remove/page.tsx
similarity index 100%
rename from frontend/src/app/settings/remove/page.tsx
rename to frontend/src/app/settings/(submenus)/remove/page.tsx
diff --git a/frontend/src/app/settings/security/page.tsx b/frontend/src/app/settings/(submenus)/security/page.tsx
similarity index 100%
rename from frontend/src/app/settings/security/page.tsx
rename to frontend/src/app/settings/(submenus)/security/page.tsx
diff --git a/frontend/src/app/settings/loading.tsx b/frontend/src/app/settings/loading.tsx
new file mode 100644
index 000000000..5df2a7350
--- /dev/null
+++ b/frontend/src/app/settings/loading.tsx
@@ -0,0 +1,21 @@
+import HeaderSpacer from "@/features/header/components/header-spacer";
+
+export default function Loading() {
+ return (
+
+
+
+
+
Account Settings
+
+ Manage your account settings and preferences!
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/text-input-field.tsx b/frontend/src/components/text-input-field.tsx
index 9bc27d9c4..b6df1de8d 100644
--- a/frontend/src/components/text-input-field.tsx
+++ b/frontend/src/components/text-input-field.tsx
@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from "react";
-import * as Collapsible from "@radix-ui/react-collapsible";
import { EyeClosedIcon, EyeIcon, TriangleAlertIcon } from "lucide-react";
import PasswordCriteria from "@/features/auth/components/password-criteria";
@@ -192,14 +191,10 @@ export default function TextInputField(props: TextInputFieldProps) {
)}
- {/* Password Criteria */}
-
-
-
-
-
+
);
}
diff --git a/frontend/src/features/auth/components/password-criteria.tsx b/frontend/src/features/auth/components/password-criteria.tsx
index 932d13ae8..153b58a80 100644
--- a/frontend/src/features/auth/components/password-criteria.tsx
+++ b/frontend/src/features/auth/components/password-criteria.tsx
@@ -1,44 +1,69 @@
+import { AnimatePresence, motion } from "framer-motion";
import { CheckIcon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils/classname";
type PasswordCriteriaProps = {
+ open: boolean;
criteria: { [key: string]: boolean };
};
-export default function PasswordCriteria(props: PasswordCriteriaProps) {
- const allCriteriaMet = Object.values(props.criteria).every((value) => value);
+export default function PasswordCriteria({
+ open,
+ criteria,
+}: PasswordCriteriaProps) {
+ const allCriteriaMet = Object.values(criteria).every((value) => value);
const iconClass = "w-4 h-4";
return (
-
- {allCriteriaMet ? (
-
-
- Password is strong!
-
- ) : (
- <>
-
Your password must:
- {Object.entries(props.criteria).map(([key, value], index) => (
-
- {value ? (
-
- ) : (
-
- )}
- {key}
-
- ))}
- >
+
+ {open && (
+
+
+
+
+ {allCriteriaMet ? (
+
+
+ Your password is strong!
+
+ ) : (
+ <>
+ Your password must:
+ {Object.entries(criteria).map(([key, value], index) => (
+
+ {value ? (
+
+ ) : (
+
+ )}
+ {key}
+
+ ))}
+ >
+ )}
+
+
+
+
)}
-
+
);
}
diff --git a/frontend/src/features/drawer/components/base.tsx b/frontend/src/features/drawer/components/base.tsx
index d323b5f82..04d03d6ce 100644
--- a/frontend/src/features/drawer/components/base.tsx
+++ b/frontend/src/features/drawer/components/base.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { XIcon } from "lucide-react";
import { Drawer } from "vaul";
@@ -35,11 +35,21 @@ export default function BaseDrawer({
const contentRef = useRef(null);
const wasDraggingRef = useRef(false);
+ const animatingTimeoutRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
useVaulStickyFooter(contentRef, isDragging || isAnimating);
+ // Animating timeout cleanup
+ useEffect(() => {
+ return () => {
+ if (animatingTimeoutRef.current) {
+ clearTimeout(animatingTimeoutRef.current);
+ }
+ };
+ }, []);
+
/**
* CONDITIONAL PROPS BASED ON VARIANT
*
@@ -188,9 +198,17 @@ export default function BaseDrawer({
onClick={() => {
if (wasDraggingRef.current) return;
if (isPill) {
+ if (animatingTimeoutRef.current) {
+ clearTimeout(animatingTimeoutRef.current);
+ }
+
setIsAnimating(true);
setSnap(snapPoints?.[1] ?? null);
- setIsAnimating(false);
+ // Let the footer observer update before setting isAnimating to false
+ animatingTimeoutRef.current = setTimeout(() => {
+ setIsAnimating(false);
+ animatingTimeoutRef.current = null;
+ }, 0);
}
}}
className={cn(
diff --git a/frontend/src/features/event/editor/constants.ts b/frontend/src/features/event/editor/constants.ts
new file mode 100644
index 000000000..f68f9c05d
--- /dev/null
+++ b/frontend/src/features/event/editor/constants.ts
@@ -0,0 +1,4 @@
+const MAX_DAYS = 64;
+export const MAX_DURATION_MS = MAX_DAYS * 24 * 60 * 60 * 1000;
+export const MAX_DURATION = `${MAX_DAYS} days`;
+export const MAX_TITLE_LENGTH = 50;
diff --git a/frontend/src/features/event/editor/validate-data.ts b/frontend/src/features/event/editor/validate-data.ts
index f201de55f..503c35f41 100644
--- a/frontend/src/features/event/editor/validate-data.ts
+++ b/frontend/src/features/event/editor/validate-data.ts
@@ -1,14 +1,12 @@
import { EventInformation, WeekdayRange } from "@/core/event/types";
import { findRangeFromWeekdayArray } from "@/core/event/weekday-utils";
+import {
+ MAX_DURATION_MS,
+ MAX_TITLE_LENGTH,
+} from "@/features/event/editor/constants";
import { EventEditorType } from "@/features/event/editor/types";
import { MESSAGES } from "@/lib/messages";
-const MAX_DAYS = 64;
-export const MAX_DURATION_MS = MAX_DAYS * 24 * 60 * 60 * 1000;
-export const MAX_DURATION = `${MAX_DAYS} days`;
-
-export const MAX_TITLE_LENGTH = 50;
-
export async function validateEventData(
editorType: EventEditorType,
data: EventInformation,
diff --git a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx
index 68fb0cb77..648a3db0c 100644
--- a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx
+++ b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx
@@ -4,6 +4,7 @@ import ActionButton from "@/features/button/components/action";
import BaseModal from "@/features/system-feedback/dialog/components/base";
import { DIALOG_CONFIG } from "@/features/system-feedback/dialog/config";
import { ConfirmationDialogProps } from "@/features/system-feedback/dialog/props";
+import useCheckMobile from "@/lib/hooks/use-check-mobile";
import { cn } from "@/lib/utils/classname";
export default function ConfirmationDialog({
@@ -26,6 +27,11 @@ export default function ConfirmationDialog({
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
+ const isMobile = useCheckMobile();
+ if (isMobile && !asNestedDrawer) {
+ asNestedDrawer = true;
+ }
+
const handleOpenChange = useCallback(
(newOpen: boolean) => {
if (!isControlled) {
diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx
index ece3bf848..6c1542db6 100644
--- a/frontend/src/features/system-feedback/toast/base.tsx
+++ b/frontend/src/features/system-feedback/toast/base.tsx
@@ -107,7 +107,8 @@ export default function BaseToast({
}
}}
className={cn(
- "z-10 col-start-3 row-span-2 flex h-6 w-6 items-center justify-center rounded-full",
+ "z-10 col-start-3 row-span-2 flex h-6 w-6",
+ "cursor-pointer items-center justify-center rounded-full",
"hover:bg-black/20 focus:outline-none focus:ring-2 focus:ring-white/50",
!isPersistent &&
"opacity-0 focus-visible:opacity-100 group-hover:opacity-100",
diff --git a/frontend/src/features/version-history/data.ts b/frontend/src/features/version-history/data.ts
index 78cdf38cd..d4f833d97 100644
--- a/frontend/src/features/version-history/data.ts
+++ b/frontend/src/features/version-history/data.ts
@@ -183,6 +183,17 @@ export function getVersionHistoryData(): VersionHistoryData {
"Removed default dates and times from the new event page",
],
},
+ {
+ version: "v0.4.5",
+ releaseDate: { year: 2026, month: 5, day: 29 },
+ changes: [
+ "Added animations to the password strength criteria",
+ "Updated confirmation dialogs to display as drawers on mobile",
+ "Updated loading page skeletons to be consistent with new page layouts",
+ "Updated the authentication code checking rate limit to prevent lockouts",
+ "Fixed an issue where the mobile results page drawer would not display the footer when opened",
+ ],
+ },
],
},
];
diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts
new file mode 100644
index 000000000..eb19293fc
--- /dev/null
+++ b/frontend/src/global.d.ts
@@ -0,0 +1,2 @@
+// Silences an error when importing globals.css in layout.tsx
+declare module "*.css";
diff --git a/frontend/src/lib/messages.ts b/frontend/src/lib/messages.ts
index cef5a4f48..f7f4c9b84 100644
--- a/frontend/src/lib/messages.ts
+++ b/frontend/src/lib/messages.ts
@@ -2,7 +2,7 @@ import { MAX_DEFAULT_NAME_LENGTH } from "@/features/account/constants";
import {
MAX_DURATION,
MAX_TITLE_LENGTH,
-} from "@/features/event/editor/validate-data";
+} from "@/features/event/editor/constants";
export const MESSAGES = {
// generic errors