mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-31 12:48:03 +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 { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useTheme } from "@/hooks/use-theme";
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
import { Moon, Sun } from "lucide-react";
|
import { Moon, Sun, Laptop } from "lucide-react";
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
setTheme(theme === "dark" ? "light" : "dark");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="outline"
|
<DropdownMenuTrigger asChild>
|
||||||
size="icon"
|
<Button variant="outline" size="icon" title="Change theme">
|
||||||
onClick={toggleTheme}
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:rotate-90 dark:scale-0" />
|
||||||
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
<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>
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
</Button>
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
</DropdownMenuTrigger>
|
||||||
<span className="sr-only">Toggle theme</span>
|
<DropdownMenuContent align="end">
|
||||||
</Button>
|
<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 { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||||
import { Organization } from "@shared/schema";
|
import { Organization, InsertOrganization } from "@shared/schema";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
type OrganizationContextType = {
|
type OrganizationContextType = {
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
currentOrganization: Organization | null;
|
currentOrganization: Organization | null;
|
||||||
setCurrentOrganization: (organization: Organization) => void;
|
setCurrentOrganization: (organization: Organization) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
createOrganizationMutation: UseMutationResult<Organization, Error, InsertOrganization>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OrganizationContext = createContext<OrganizationContextType | undefined>(undefined);
|
const OrganizationContext = createContext<OrganizationContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function OrganizationProvider({ children }: { children: ReactNode }) {
|
export function OrganizationProvider({ children }: { children: ReactNode }) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { toast } = useToast();
|
||||||
const [currentOrganization, setCurrentOrganization] = useState<Organization | null>(null);
|
const [currentOrganization, setCurrentOrganization] = useState<Organization | null>(null);
|
||||||
|
|
||||||
const { data: organizations = [], isLoading } = useQuery<Organization[]>({
|
const { data: organizations = [], isLoading } = useQuery<Organization[]>({
|
||||||
@@ -21,6 +25,28 @@ export function OrganizationProvider({ children }: { children: ReactNode }) {
|
|||||||
// Only fetch if user is logged in
|
// Only fetch if user is logged in
|
||||||
enabled: !!user,
|
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
|
// Set default organization when organizations are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,7 +67,13 @@ export function OrganizationProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<OrganizationContext.Provider
|
<OrganizationContext.Provider
|
||||||
value={{ organizations, currentOrganization, setCurrentOrganization, isLoading }}
|
value={{
|
||||||
|
organizations,
|
||||||
|
currentOrganization,
|
||||||
|
setCurrentOrganization,
|
||||||
|
isLoading,
|
||||||
|
createOrganizationMutation
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</OrganizationContext.Provider>
|
</OrganizationContext.Provider>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Separator } from "@/components/ui/separator";
|
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
|
// Organization form schema
|
||||||
const organizationFormSchema = z.object({
|
const organizationFormSchema = z.object({
|
||||||
@@ -61,6 +61,12 @@ const organizationFormSchema = z.object({
|
|||||||
isActive: z.boolean().default(true),
|
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
|
// Account form schema
|
||||||
const accountFormSchema = z.object({
|
const accountFormSchema = z.object({
|
||||||
username: z.string().min(3, "Username must be at least 3 characters"),
|
username: z.string().min(3, "Username must be at least 3 characters"),
|
||||||
@@ -82,10 +88,19 @@ export default function SettingsPage() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { currentOrganization, setCurrentOrganization } = useOrganization();
|
const { currentOrganization, setCurrentOrganization, createOrganizationMutation } = useOrganization();
|
||||||
|
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
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
|
// Fetch organization data
|
||||||
const { data: organization } = useQuery<Organization>({
|
const { data: organization } = useQuery<Organization>({
|
||||||
queryKey: ["/api/organizations", currentOrganization?.id],
|
queryKey: ["/api/organizations", currentOrganization?.id],
|
||||||
@@ -434,6 +449,77 @@ export default function SettingsPage() {
|
|||||||
</Card>
|
</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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Organization Details</CardTitle>
|
<CardTitle>Organization Details</CardTitle>
|
||||||
@@ -546,7 +632,7 @@ export default function SettingsPage() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Select your preferred theme appearance
|
Select your preferred theme appearance
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
<div className="grid grid-cols-3 gap-4 pt-2">
|
||||||
<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 ${
|
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" : ""
|
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>
|
<p className="text-xs text-muted-foreground">Dark background with light text</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"variant": "professional",
|
"variant": "professional",
|
||||||
"primary": "hsl(221.2 83.2% 53.3%)",
|
"primary": "hsl(221.2 83.2% 53.3%)",
|
||||||
"appearance": "light",
|
"appearance": "system",
|
||||||
"radius": 0.5
|
"radius": 0.5
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user