mirror of
				https://github.com/ItzCrazyKns/Perplexica.git
				synced 2025-10-31 03:18:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			369 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			369 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|   SelectUIConfigField,
 | |
|   StringUIConfigField,
 | |
|   SwitchUIConfigField,
 | |
|   TextareaUIConfigField,
 | |
|   UIConfigField,
 | |
| } from '@/lib/config/types';
 | |
| import { useState } from 'react';
 | |
| import Select from '../ui/Select';
 | |
| import { toast } from 'sonner';
 | |
| import { useTheme } from 'next-themes';
 | |
| import { Loader2 } from 'lucide-react';
 | |
| import { Switch } from '@headlessui/react';
 | |
| 
 | |
| const SettingsSelect = ({
 | |
|   field,
 | |
|   value,
 | |
|   setValue,
 | |
|   dataAdd,
 | |
| }: {
 | |
|   field: SelectUIConfigField;
 | |
|   value?: any;
 | |
|   setValue: (value: any) => void;
 | |
|   dataAdd: string;
 | |
| }) => {
 | |
|   const [loading, setLoading] = useState(false);
 | |
|   const { setTheme } = useTheme();
 | |
| 
 | |
|   const handleSave = async (newValue: any) => {
 | |
|     setLoading(true);
 | |
|     setValue(newValue);
 | |
|     try {
 | |
|       if (field.scope === 'client') {
 | |
|         localStorage.setItem(field.key, newValue);
 | |
|         if (field.key === 'theme') {
 | |
|           setTheme(newValue);
 | |
|         }
 | |
|       } else {
 | |
|         const res = await fetch('/api/config', {
 | |
|           method: 'POST',
 | |
|           headers: {
 | |
|             'Content-Type': 'application/json',
 | |
|           },
 | |
|           body: JSON.stringify({
 | |
|             key: `${dataAdd}.${field.key}`,
 | |
|             value: newValue,
 | |
|           }),
 | |
|         });
 | |
| 
 | |
|         if (!res.ok) {
 | |
|           console.error('Failed to save config:', await res.text());
 | |
|           throw new Error('Failed to save configuration');
 | |
|         }
 | |
|       }
 | |
|     } catch (error) {
 | |
|       console.error('Error saving config:', error);
 | |
|       toast.error('Failed to save configuration.');
 | |
|     } finally {
 | |
|       setTimeout(() => setLoading(false), 150);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
 | |
|       <div className="space-y-3 lg:space-y-5">
 | |
|         <div>
 | |
|           <h4 className="text-sm lg:text-base text-black dark:text-white">
 | |
|             {field.name}
 | |
|           </h4>
 | |
|           <p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
 | |
|             {field.description}
 | |
|           </p>
 | |
|         </div>
 | |
|         <Select
 | |
|           value={value}
 | |
|           onChange={(event) => handleSave(event.target.value)}
 | |
|           options={field.options.map((option) => ({
 | |
|             value: option.value,
 | |
|             label: option.name,
 | |
|           }))}
 | |
|           className="!text-xs lg:!text-sm"
 | |
|           loading={loading}
 | |
|           disabled={loading}
 | |
|         />
 | |
|       </div>
 | |
|     </section>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const SettingsInput = ({
 | |
|   field,
 | |
|   value,
 | |
|   setValue,
 | |
|   dataAdd,
 | |
| }: {
 | |
|   field: StringUIConfigField;
 | |
|   value?: any;
 | |
|   setValue: (value: any) => void;
 | |
|   dataAdd: string;
 | |
| }) => {
 | |
|   const [loading, setLoading] = useState(false);
 | |
| 
 | |
|   const handleSave = async (newValue: any) => {
 | |
|     setLoading(true);
 | |
|     setValue(newValue);
 | |
|     try {
 | |
|       if (field.scope === 'client') {
 | |
|         localStorage.setItem(field.key, newValue);
 | |
|       } else {
 | |
|         const res = await fetch('/api/config', {
 | |
|           method: 'POST',
 | |
|           headers: {
 | |
|             'Content-Type': 'application/json',
 | |
|           },
 | |
|           body: JSON.stringify({
 | |
|             key: `${dataAdd}.${field.key}`,
 | |
|             value: newValue,
 | |
|           }),
 | |
|         });
 | |
| 
 | |
|         if (!res.ok) {
 | |
|           console.error('Failed to save config:', await res.text());
 | |
|           throw new Error('Failed to save configuration');
 | |
|         }
 | |
|       }
 | |
|     } catch (error) {
 | |
|       console.error('Error saving config:', error);
 | |
|       toast.error('Failed to save configuration.');
 | |
|     } finally {
 | |
|       setTimeout(() => setLoading(false), 150);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
 | |
|       <div className="space-y-3 lg:space-y-5">
 | |
|         <div>
 | |
|           <h4 className="text-sm lg:text-base text-black dark:text-white">
 | |
|             {field.name}
 | |
|           </h4>
 | |
|           <p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
 | |
|             {field.description}
 | |
|           </p>
 | |
|         </div>
 | |
|         <div className="relative">
 | |
|           <input
 | |
|             value={value ?? field.default ?? ''}
 | |
|             onChange={(event) => setValue(event.target.value)}
 | |
|             onBlur={(event) => handleSave(event.target.value)}
 | |
|             className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
 | |
|             placeholder={field.placeholder}
 | |
|             type="text"
 | |
|             disabled={loading}
 | |
|           />
 | |
|           {loading && (
 | |
|             <span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40">
 | |
|               <Loader2 className="h-4 w-4 animate-spin" />
 | |
|             </span>
 | |
|           )}
 | |
|         </div>
 | |
|       </div>
 | |
|     </section>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const SettingsTextarea = ({
 | |
|   field,
 | |
|   value,
 | |
|   setValue,
 | |
|   dataAdd,
 | |
| }: {
 | |
|   field: TextareaUIConfigField;
 | |
|   value?: any;
 | |
|   setValue: (value: any) => void;
 | |
|   dataAdd: string;
 | |
| }) => {
 | |
|   const [loading, setLoading] = useState(false);
 | |
| 
 | |
|   const handleSave = async (newValue: any) => {
 | |
|     setLoading(true);
 | |
|     setValue(newValue);
 | |
|     try {
 | |
|       if (field.scope === 'client') {
 | |
|         localStorage.setItem(field.key, newValue);
 | |
|       } else {
 | |
|         const res = await fetch('/api/config', {
 | |
|           method: 'POST',
 | |
|           headers: {
 | |
|             'Content-Type': 'application/json',
 | |
|           },
 | |
|           body: JSON.stringify({
 | |
|             key: `${dataAdd}.${field.key}`,
 | |
|             value: newValue,
 | |
|           }),
 | |
|         });
 | |
| 
 | |
|         if (!res.ok) {
 | |
|           console.error('Failed to save config:', await res.text());
 | |
|           throw new Error('Failed to save configuration');
 | |
|         }
 | |
|       }
 | |
|     } catch (error) {
 | |
|       console.error('Error saving config:', error);
 | |
|       toast.error('Failed to save configuration.');
 | |
|     } finally {
 | |
|       setTimeout(() => setLoading(false), 150);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
 | |
|       <div className="space-y-3 lg:space-y-5">
 | |
|         <div>
 | |
|           <h4 className="text-sm lg:text-base text-black dark:text-white">
 | |
|             {field.name}
 | |
|           </h4>
 | |
|           <p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
 | |
|             {field.description}
 | |
|           </p>
 | |
|         </div>
 | |
|         <div className="relative">
 | |
|           <textarea
 | |
|             value={value ?? field.default ?? ''}
 | |
|             onChange={(event) => setValue(event.target.value)}
 | |
|             onBlur={(event) => handleSave(event.target.value)}
 | |
|             className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
 | |
|             placeholder={field.placeholder}
 | |
|             rows={4}
 | |
|             disabled={loading}
 | |
|           />
 | |
|           {loading && (
 | |
|             <span className="pointer-events-none absolute right-3 translate-y-3 text-black/40 dark:text-white/40">
 | |
|               <Loader2 className="h-4 w-4 animate-spin" />
 | |
|             </span>
 | |
|           )}
 | |
|         </div>
 | |
|       </div>
 | |
|     </section>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const SettingsSwitch = ({
 | |
|   field,
 | |
|   value,
 | |
|   setValue,
 | |
|   dataAdd,
 | |
| }: {
 | |
|   field: SwitchUIConfigField;
 | |
|   value?: any;
 | |
|   setValue: (value: any) => void;
 | |
|   dataAdd: string;
 | |
| }) => {
 | |
|   const [loading, setLoading] = useState(false);
 | |
| 
 | |
|   const handleSave = async (newValue: boolean) => {
 | |
|     setLoading(true);
 | |
|     setValue(newValue);
 | |
|     try {
 | |
|       if (field.scope === 'client') {
 | |
|         localStorage.setItem(field.key, String(newValue));
 | |
|       } else {
 | |
|         const res = await fetch('/api/config', {
 | |
|           method: 'POST',
 | |
|           headers: {
 | |
|             'Content-Type': 'application/json',
 | |
|           },
 | |
|           body: JSON.stringify({
 | |
|             key: `${dataAdd}.${field.key}`,
 | |
|             value: newValue,
 | |
|           }),
 | |
|         });
 | |
| 
 | |
|         if (!res.ok) {
 | |
|           console.error('Failed to save config:', await res.text());
 | |
|           throw new Error('Failed to save configuration');
 | |
|         }
 | |
|       }
 | |
|     } catch (error) {
 | |
|       console.error('Error saving config:', error);
 | |
|       toast.error('Failed to save configuration.');
 | |
|     } finally {
 | |
|       setTimeout(() => setLoading(false), 150);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const isChecked = value === true || value === 'true';
 | |
| 
 | |
|   return (
 | |
|     <section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
 | |
|       <div className="flex flex-row items-center space-x-3 lg:space-x-5 w-full justify-between">
 | |
|         <div>
 | |
|           <h4 className="text-sm lg:text-base text-black dark:text-white">
 | |
|             {field.name}
 | |
|           </h4>
 | |
|           <p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
 | |
|             {field.description}
 | |
|           </p>
 | |
|         </div>
 | |
|         <Switch
 | |
|           checked={isChecked}
 | |
|           onChange={handleSave}
 | |
|           disabled={loading}
 | |
|           className="group relative flex h-6 w-12 shrink-0 cursor-pointer rounded-full bg-white/10 p-1 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500"
 | |
|         >
 | |
|           <span
 | |
|             aria-hidden="true"
 | |
|             className="pointer-events-none inline-block size-4 translate-x-0 rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out group-data-[checked]:translate-x-6"
 | |
|           />
 | |
|         </Switch>
 | |
|       </div>
 | |
|     </section>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const SettingsField = ({
 | |
|   field,
 | |
|   value,
 | |
|   dataAdd,
 | |
| }: {
 | |
|   field: UIConfigField;
 | |
|   value: any;
 | |
|   dataAdd: string;
 | |
| }) => {
 | |
|   const [val, setVal] = useState(value);
 | |
| 
 | |
|   switch (field.type) {
 | |
|     case 'select':
 | |
|       return (
 | |
|         <SettingsSelect
 | |
|           field={field}
 | |
|           value={val}
 | |
|           setValue={setVal}
 | |
|           dataAdd={dataAdd}
 | |
|         />
 | |
|       );
 | |
|     case 'string':
 | |
|       return (
 | |
|         <SettingsInput
 | |
|           field={field}
 | |
|           value={val}
 | |
|           setValue={setVal}
 | |
|           dataAdd={dataAdd}
 | |
|         />
 | |
|       );
 | |
|     case 'textarea':
 | |
|       return (
 | |
|         <SettingsTextarea
 | |
|           field={field}
 | |
|           value={val}
 | |
|           setValue={setVal}
 | |
|           dataAdd={dataAdd}
 | |
|         />
 | |
|       );
 | |
|     case 'switch':
 | |
|       return (
 | |
|         <SettingsSwitch
 | |
|           field={field}
 | |
|           value={val}
 | |
|           setValue={setVal}
 | |
|           dataAdd={dataAdd}
 | |
|         />
 | |
|       );
 | |
|     default:
 | |
|       return <div>Unsupported field type: {field.type}</div>;
 | |
|   }
 | |
| };
 | |
| 
 | |
| export default SettingsField;
 |