From 6c76721733dc4bacfb553db0293a463a179b5f0d Mon Sep 17 00:00:00 2001 From: BangKartavya Date: Fri, 21 Aug 2026 13:05:46 +0530 Subject: [PATCH 1/3] add : ability to disable and enable features --- src/components/Admin.tsx | 200 +++++++++++++------- src/components/FeatureDisabled.tsx | 44 +++++ src/lib/supabase/actions/flags.actions.ts | 41 ++++ src/pages/admin/components/FeatureFlags.tsx | 173 +++++++++++++++++ src/pages/admin/components/Role.tsx | 8 +- src/pages/apply.tsx | 18 +- src/types/index.d.ts | 8 + 7 files changed, 417 insertions(+), 75 deletions(-) create mode 100644 src/components/FeatureDisabled.tsx create mode 100644 src/lib/supabase/actions/flags.actions.ts create mode 100644 src/pages/admin/components/FeatureFlags.tsx diff --git a/src/components/Admin.tsx b/src/components/Admin.tsx index 63359ee..d848b7d 100644 --- a/src/components/Admin.tsx +++ b/src/components/Admin.tsx @@ -14,7 +14,8 @@ import PageHead from "./layout/PageHead"; import PageSection from "./layout/PageSection"; import ResourceEditor from "@/pages/admin/components/ResourceEditor"; import BlogsEditor from "@/pages/admin/components/BlogsEditor"; -import Role from "@/pages/admin/components/Role"; +import RolesEditor from "@/pages/admin/components/Role"; +import FeatureFlags from "@/pages/admin/components/FeatureFlags"; export enum TabValues { HERO="hero", @@ -24,10 +25,131 @@ export enum TabValues { TEAM="team", BLOGS = "blogs", RESOURCES="resources", - ROLES="roles" + ROLES="roles", + FEATURE_FLAGS="feature_flags" } +const tabComponentMap: Record = { + [TabValues.HERO]: ( + + + + ), + [TabValues.PROJECTS]: ( + + + + ), + [TabValues.ACTIVITIES]: ( + + + + ), + [TabValues.EVENTS]: ( + + + + ), + [TabValues.TEAM]: ( + + + + ), + [TabValues.BLOGS]: ( + + + + ), + [TabValues.RESOURCES]: ( + + + + ), + [TabValues.ROLES]: ( + + + + ), + [TabValues.FEATURE_FLAGS]: ( + + + + ), +} + +const tabTriggerMap: Record = { + [TabValues.HERO]: ( + + Hero Section + + ), + [TabValues.PROJECTS]: ( + + Projects + + ), + [TabValues.ACTIVITIES]: ( + + Activities + + ), + [TabValues.EVENTS]: ( + + Events + + ), + [TabValues.TEAM]: ( + + Team + + ), + [TabValues.BLOGS]: ( + + Blogs + + ), + [TabValues.RESOURCES]: ( + + Resources + + ), + [TabValues.ROLES]: ( + + Roles + + ), + [TabValues.FEATURE_FLAGS]: ( + + Feature Flags + + ), +} + +const titleMap: Record = { + [TabValues.HERO]: "Hero Section Editor", + [TabValues.PROJECTS]: "Projects Editor", + [TabValues.ACTIVITIES]: "Activities Editor", + [TabValues.EVENTS]: "Events Editor", + [TabValues.TEAM]: "Team Members Editor", + [TabValues.BLOGS]: "Blogs Editor", + [TabValues.RESOURCES]: "Add Resources", + [TabValues.ROLES]: "Define Roles", + [TabValues.FEATURE_FLAGS]: "Feature Flags", +} + +const descriptionMap: Record = { + [TabValues.HERO]:"Edit the main heading, text and background images", + [TabValues.PROJECTS]:"Add, edit or remove projects", + [TabValues.ACTIVITIES]:"Manage robotics activities and workshops", + [TabValues.EVENTS]:"Add or edit upcoming events and competitions", + [TabValues.TEAM]:"Manage team members and their information", + [TabValues.BLOGS]:"Manage blog posts", + [TabValues.RESOURCES]:"Manage Resources and Links", + [TabValues.ROLES]:"Manage Roles for different society members", + [TabValues.FEATURE_FLAGS]: "Manage Enabling and Disabling Features", +} + const Admin = () => { const [activeTab, setActiveTab] = useState(TabValues.HERO); return ( @@ -52,85 +174,23 @@ const Admin = () => { setActiveTab(value as TabValues)} className="w-full mb-12">
- - Hero Section - - - Projects - - - Activities - - - Events - - - Team - - - Blogs - - - Resources - - - Roles - + { + ...Object.entries(tabTriggerMap).map((object => object[1])) + }
- {activeTab === TabValues.HERO && "Hero Section Editor"} - {activeTab === TabValues.PROJECTS && "Projects Editor"} - {activeTab === TabValues.ACTIVITIES && "Activities Editor"} - {activeTab === TabValues.EVENTS && "Events Editor"} - {activeTab === TabValues.TEAM && "Team Members Editor"} - {activeTab === TabValues.BLOGS && "Blogs Editor"} - {activeTab === TabValues.RESOURCES && "Add Resources"} - {activeTab === TabValues.ROLES && "Define Roles"} + {titleMap[activeTab]} - {activeTab === TabValues.HERO && "Edit the main heading, text and background images"} - {activeTab === TabValues.PROJECTS && "Add, edit or remove projects"} - {activeTab === TabValues.ACTIVITIES && "Manage robotics activities and workshops"} - {activeTab === TabValues.EVENTS && "Add or edit upcoming events and competitions"} - {activeTab === TabValues.TEAM && "Manage team members and their information"} - {activeTab === TabValues.BLOGS && "Manage blog posts"} - {activeTab === TabValues.RESOURCES && "Manage Resources and Links"} - {activeTab === TabValues.ROLES && "Manage Roles for different society members"} + {descriptionMap[activeTab]} - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {tabComponentMap[activeTab]}
diff --git a/src/components/FeatureDisabled.tsx b/src/components/FeatureDisabled.tsx new file mode 100644 index 0000000..9134aef --- /dev/null +++ b/src/components/FeatureDisabled.tsx @@ -0,0 +1,44 @@ +import { AlertCircle, Clock } from 'lucide-react'; + +interface ClosedPageCardProps { + title?: string; + message?: string; + showIcon?: boolean; +} + +const FeatureDisabled = ({ + title = "Applications Closed", + message = "This page is not currently accepting responses. Please check back later.", + showIcon = true, +}: ClosedPageCardProps) => { + return ( +
+
+ {showIcon && ( +
+
+ +
+
+ )} + +

+ {title} +

+ +

+ {message} +

+ +
+ +

+ We'll notify you when responses are accepted again. +

+
+
+
+ ); +} + +export default FeatureDisabled; \ No newline at end of file diff --git a/src/lib/supabase/actions/flags.actions.ts b/src/lib/supabase/actions/flags.actions.ts new file mode 100644 index 0000000..73d39fc --- /dev/null +++ b/src/lib/supabase/actions/flags.actions.ts @@ -0,0 +1,41 @@ +import { FeatureFlagType } from "@/types"; +import { client } from "../supabase" + +export const createFeatureFlag = async (value: Omit) => { + const {error} = await client.from("featureFlags").insert(value); + + if(error) { + console.log(error); + return error; + } + + return null; +} + +export const getFeatureFlags= async (): Promise => { + const {data,error} = await client.from("featureFlags").select("*").order("name", {ascending: true}); + + if(error) console.log(error); + + return (data ?? []) as FeatureFlagType[]; +} + +export const updateFeatureFlag = async (id: string, isEnabled: boolean) => { + const {error} = await client.from("featureFlags").update({isEnabled}).eq("id", id); + + if(error) { + console.log(error); + return error; + } + return null; +} + +export const getFeatureFlagByName = async (name: string): Promise => { + const {data,error} = await client.from("featureFlags").select("*").eq("name", name).maybeSingle(); + + if(error) { + console.log(error); + } + + return data as FeatureFlagType; +} \ No newline at end of file diff --git a/src/pages/admin/components/FeatureFlags.tsx b/src/pages/admin/components/FeatureFlags.tsx new file mode 100644 index 0000000..6f1e24a --- /dev/null +++ b/src/pages/admin/components/FeatureFlags.tsx @@ -0,0 +1,173 @@ +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Flag, Plus } from "lucide-react"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { createFeatureFlag, getFeatureFlags, updateFeatureFlag } from "@/lib/supabase/actions/flags.actions"; +import { FeatureFlagType } from "@/types"; + + +const formSchema = z.object({ + name: z.string().trim().min(1, "Name is required").max(80, "Name is too long"), + isEnabled: z.boolean(), +}); + +type FormValues = z.infer; + +const FeatureFlags = () => { + const [open, setOpen] = useState(false); + const [featureFlags, setFeatureFlags] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [key, setKey] = useState(0); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { name: "", isEnabled: true }, + }); + + useEffect(() => { + const fetch = async () => { + const flagsData = await getFeatureFlags(); + setFeatureFlags(flagsData); + } + + fetch(); + }, [key]); + + return ( +
+
+
+

Feature flags

+

+ Turn features on or off across the app. +

+
+ +
+ + + + {featureFlags.length === 0 ? ( +
+ +

No feature flags yet

+

+ Create your first flag to start rolling out features. +

+
+ ) : ( +
    + {featureFlags.map((flag) => ( +
  • +
    +

    {flag.name}

    + + {flag.isEnabled ? "Enabled" : "Disabled"} + +
    + { + const error = await updateFeatureFlag(flag.id,checked); + + if(error) { + toast.error(error.message); + } + + setKey(key => key+1); + }} + /> +
  • + ))} +
+ )} +
+
+ + + +
{ + setIsSubmitting(true); + const error = await createFeatureFlag(values); + + if(error) { + toast.error(error.message); + } + setIsSubmitting(false); + setOpen(false); + setKey(key => key+1); + })}> + + Add feature + + Give the feature a name and choose its initial status. + + + +
+
+ + + {form.formState.errors.name ? ( +

{form.formState.errors.name.message}

+ ) : null} +
+ +
+
+ +

+ {form.watch("isEnabled") ? "Enabled" : "Disabled"} +

+
+ + form.setValue("isEnabled", checked, { shouldDirty: true }) + } + /> +
+
+ + + + + +
+
+
+
+ ); +} + +export default FeatureFlags; \ No newline at end of file diff --git a/src/pages/admin/components/Role.tsx b/src/pages/admin/components/Role.tsx index 691d28b..e2f814c 100644 --- a/src/pages/admin/components/Role.tsx +++ b/src/pages/admin/components/Role.tsx @@ -6,7 +6,7 @@ import { ShieldAlert } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { fetchRoles, type Role } from "@/lib/roles"; +import { fetchRoles, type Role as RolesEditor } from "@/lib/roles"; import { useAuthRole } from "@/lib/useAuthRole"; import RolesPanel from "./role/RolesPanel"; import UsersPanel from "./role/UsersPanel"; @@ -18,9 +18,9 @@ import UsersPanel from "./role/UsersPanel"; * ... * */ -const Role = () => { +const RolesEditor = () => { const { loading, isAdmin } = useAuthRole(); - const [roles, setRoles] = useState([]); + const [roles, setRoles] = useState([]); useEffect(() => { if (!isAdmin) return; @@ -77,4 +77,4 @@ const Role = () => { ); } -export default Role; \ No newline at end of file +export default RolesEditor; \ No newline at end of file diff --git a/src/pages/apply.tsx b/src/pages/apply.tsx index 297c8f2..3f53c08 100644 --- a/src/pages/apply.tsx +++ b/src/pages/apply.tsx @@ -5,12 +5,16 @@ import PageSection from "@/components/layout/PageSection"; import ApplicationForm from "@/components/ApplicationForm"; import { Button } from "@/components/ui/button"; import { client } from "@/lib/supabase/supabase"; +import { getFeatureFlagByName } from "@/lib/supabase/actions/flags.actions"; +import { FeatureFlagType } from "@/types"; +import FeatureDisabled from "@/components/FeatureDisabled"; export default function ApplyPage() { const router = useRouter(); const [loading, setLoading] = useState(true); const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isFeatureActive, setIsFeatureActive] = useState(true); useEffect(() => { let active = true; @@ -33,6 +37,18 @@ export default function ApplyPage() { }; }, []); + useEffect(() => { + const fetch = async () => { + const feature = await getFeatureFlagByName("recruitment-application-2026"); + + if(feature) { + setIsFeatureActive(feature.isEnabled); + } + } + + fetch(); + }, []); + if (loading) { return (
@@ -85,7 +101,7 @@ export default function ApplyPage() { title="Join Robotics Society" subtitle="Apply to become a member of the Robotics Society at PEC." > - + {isFeatureActive ? : }
); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 1140d10..44ec596 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -187,3 +187,11 @@ export interface PanelistAuthType { name: string; panel_number: number; } + +export interface FeatureFlagType { + id: string; + created_at: Date; + updatedAt: Date; + name: string; + isEnabled: boolean; +} \ No newline at end of file From 06696107bd0fa326efba0e37b1b7f74bf6a6db02 Mon Sep 17 00:00:00 2001 From: BangKartavya Date: Fri, 21 Aug 2026 13:24:40 +0530 Subject: [PATCH 2/3] fix : ui flashing on apply page --- src/pages/apply.tsx | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pages/apply.tsx b/src/pages/apply.tsx index 3f53c08..49279cd 100644 --- a/src/pages/apply.tsx +++ b/src/pages/apply.tsx @@ -6,7 +6,6 @@ import ApplicationForm from "@/components/ApplicationForm"; import { Button } from "@/components/ui/button"; import { client } from "@/lib/supabase/supabase"; import { getFeatureFlagByName } from "@/lib/supabase/actions/flags.actions"; -import { FeatureFlagType } from "@/types"; import FeatureDisabled from "@/components/FeatureDisabled"; export default function ApplyPage() { @@ -15,6 +14,7 @@ export default function ApplyPage() { const [loading, setLoading] = useState(true); const [isLoggedIn, setIsLoggedIn] = useState(false); const [isFeatureActive, setIsFeatureActive] = useState(true); + const [checksComplete, setChecksComplete] = useState(false); useEffect(() => { let active = true; @@ -38,18 +38,25 @@ export default function ApplyPage() { }, []); useEffect(() => { - const fetch = async () => { - const feature = await getFeatureFlagByName("recruitment-application-2026"); - - if(feature) { - setIsFeatureActive(feature.isEnabled); + const checkFeatureFlag = async () => { + try { + const feature = await getFeatureFlagByName("recruitment-application-2026"); + if (feature) { + setIsFeatureActive(feature.isEnabled); + } + } catch (error) { + console.error("Failed to fetch feature flag:", error); + // Fail safe: disable feature if check fails + setIsFeatureActive(false); + } finally { + setChecksComplete(true); // Mark as done } - } + }; - fetch(); + checkFeatureFlag(); }, []); - if (loading) { + if (loading || !checksComplete) { return (
Date: Fri, 21 Aug 2026 13:34:54 +0530 Subject: [PATCH 3/3] fix : tabs list --- src/components/Admin.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/Admin.tsx b/src/components/Admin.tsx index d848b7d..c24aa42 100644 --- a/src/components/Admin.tsx +++ b/src/components/Admin.tsx @@ -78,53 +78,53 @@ const tabComponentMap: Record = { ), } -const tabTriggerMap: Record = { - [TabValues.HERO]: ( +const tabTriggerList: React.ReactNode[] = [ + ( Hero Section ), - [TabValues.PROJECTS]: ( + ( Projects ), - [TabValues.ACTIVITIES]: ( + ( Activities ), - [TabValues.EVENTS]: ( + ( Events ), - [TabValues.TEAM]: ( + ( Team ), - [TabValues.BLOGS]: ( + ( Blogs ), - [TabValues.RESOURCES]: ( + ( Resources ), - [TabValues.ROLES]: ( + ( Roles ), - [TabValues.FEATURE_FLAGS]: ( + ( Feature Flags ), -} +] const titleMap: Record = { [TabValues.HERO]: "Hero Section Editor", @@ -174,9 +174,7 @@ const Admin = () => { setActiveTab(value as TabValues)} className="w-full mb-12">
- { - ...Object.entries(tabTriggerMap).map((object => object[1])) - } + {tabTriggerList}