Enhance theme switching and add organization creation

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/582dd4fe-d503-4ab1-81ab-a04bba654cf7.jpg
This commit is contained in:
alphaeusmote
2025-04-09 23:57:26 +00:00
parent 760550cf03
commit 7823f21eb2
4 changed files with 167 additions and 22 deletions
+30 -15
View File
@@ -1,24 +1,39 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "@/hooks/use-theme";
import { Moon, Sun } from "lucide-react";
import { Moon, Sun, Laptop } from "lucide-react";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
return (
<Button
variant="outline"
size="icon"
onClick={toggleTheme}
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" title="Change theme">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Laptop className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+35 -3
View File
@@ -1,19 +1,23 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import { useQuery } from "@tanstack/react-query";
import { Organization } from "@shared/schema";
import { useQuery, useMutation, UseMutationResult } from "@tanstack/react-query";
import { Organization, InsertOrganization } from "@shared/schema";
import { useAuth } from "@/hooks/use-auth";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
type OrganizationContextType = {
organizations: Organization[];
currentOrganization: Organization | null;
setCurrentOrganization: (organization: Organization) => void;
isLoading: boolean;
createOrganizationMutation: UseMutationResult<Organization, Error, InsertOrganization>;
};
const OrganizationContext = createContext<OrganizationContextType | undefined>(undefined);
export function OrganizationProvider({ children }: { children: ReactNode }) {
const { user } = useAuth();
const { toast } = useToast();
const [currentOrganization, setCurrentOrganization] = useState<Organization | null>(null);
const { data: organizations = [], isLoading } = useQuery<Organization[]>({
@@ -21,6 +25,28 @@ export function OrganizationProvider({ children }: { children: ReactNode }) {
// Only fetch if user is logged in
enabled: !!user,
});
const createOrganizationMutation = useMutation({
mutationFn: async (orgData: InsertOrganization) => {
const res = await apiRequest("POST", "/api/organizations", orgData);
return await res.json();
},
onSuccess: (newOrg: Organization) => {
queryClient.invalidateQueries({ queryKey: ["/api/organizations"] });
toast({
title: "Organization created",
description: `Organization '${newOrg.name}' has been created successfully.`,
});
setCurrentOrganization(newOrg);
},
onError: (error: Error) => {
toast({
title: "Failed to create organization",
description: error.message,
variant: "destructive",
});
},
});
// Set default organization when organizations are loaded
useEffect(() => {
@@ -41,7 +67,13 @@ export function OrganizationProvider({ children }: { children: ReactNode }) {
return (
<OrganizationContext.Provider
value={{ organizations, currentOrganization, setCurrentOrganization, isLoading }}
value={{
organizations,
currentOrganization,
setCurrentOrganization,
isLoading,
createOrganizationMutation
}}
>
{children}
</OrganizationContext.Provider>
+101 -3
View File
@@ -53,7 +53,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { Loader2, Moon, Sun, Lock } from "lucide-react";
import { Loader2, Moon, Sun, Lock, Laptop } from "lucide-react";
// Organization form schema
const organizationFormSchema = z.object({
@@ -61,6 +61,12 @@ const organizationFormSchema = z.object({
isActive: z.boolean().default(true),
});
// Create organization form schema
const createOrganizationFormSchema = z.object({
name: z.string().min(1, "Organization name is required"),
isActive: z.boolean().default(true),
});
// Account form schema
const accountFormSchema = z.object({
username: z.string().min(3, "Username must be at least 3 characters"),
@@ -82,10 +88,19 @@ export default function SettingsPage() {
const { toast } = useToast();
const { user } = useAuth();
const { theme, setTheme } = useTheme();
const { currentOrganization, setCurrentOrganization } = useOrganization();
const { currentOrganization, setCurrentOrganization, createOrganizationMutation } = useOrganization();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
// Create organization form
const createOrganizationForm = useForm<z.infer<typeof createOrganizationFormSchema>>({
resolver: zodResolver(createOrganizationFormSchema),
defaultValues: {
name: "",
isActive: true,
},
});
// Fetch organization data
const { data: organization } = useQuery<Organization>({
queryKey: ["/api/organizations", currentOrganization?.id],
@@ -434,6 +449,77 @@ export default function SettingsPage() {
</Card>
) : (
<>
{user?.role === "admin" && (
<Card>
<CardHeader>
<CardTitle>Create New Organization</CardTitle>
<CardDescription>
Add a new organization to your account
</CardDescription>
</CardHeader>
<CardContent>
<Form {...createOrganizationForm}>
<form
onSubmit={createOrganizationForm.handleSubmit((data) => {
createOrganizationMutation.mutate(data);
createOrganizationForm.reset();
})}
className="space-y-4"
>
<FormField
control={createOrganizationForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Organization Name</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter organization name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createOrganizationForm.control}
name="isActive"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Active Status</FormLabel>
<FormDescription>
Enable this organization upon creation
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Button
type="submit"
disabled={createOrganizationMutation.isPending}
>
{createOrganizationMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Organization"
)}
</Button>
</form>
</Form>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Organization Details</CardTitle>
@@ -546,7 +632,7 @@ export default function SettingsPage() {
<p className="text-sm text-muted-foreground">
Select your preferred theme appearance
</p>
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="grid grid-cols-3 gap-4 pt-2">
<div
className={`flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer ${
theme === "light" ? "border-primary" : ""
@@ -571,6 +657,18 @@ export default function SettingsPage() {
<p className="text-xs text-muted-foreground">Dark background with light text</p>
</div>
</div>
<div
className={`flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer ${
theme === "system" ? "border-primary" : ""
}`}
onClick={() => setTheme("system")}
>
<Laptop className="h-6 w-6 mb-3" />
<div className="space-y-1 text-center">
<h3 className="font-medium">System</h3>
<p className="text-xs text-muted-foreground">Follow your system preference</p>
</div>
</div>
</div>
</div>
</CardContent>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"variant": "professional",
"primary": "hsl(221.2 83.2% 53.3%)",
"appearance": "light",
"appearance": "system",
"radius": 0.5
}