mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-07-26 11:38:13 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[]>({
|
||||
@@ -22,6 +26,28 @@ export function OrganizationProvider({ children }: { children: ReactNode }) {
|
||||
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(() => {
|
||||
if (organizations.length > 0 && !currentOrganization) {
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"variant": "professional",
|
||||
"primary": "hsl(221.2 83.2% 53.3%)",
|
||||
"appearance": "light",
|
||||
"appearance": "system",
|
||||
"radius": 0.5
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user