|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { FloppyDiskIcon } from "@phosphor-icons/react"; |
| 3 | +import { BookOpenIcon, BuildingsIcon, FloppyDiskIcon } from "@phosphor-icons/react"; |
4 | 4 | import { useState } from "react"; |
5 | 5 | import { toast } from "sonner"; |
6 | 6 | import { Button } from "@/components/ui/button"; |
7 | | - |
8 | 7 | import { Input } from "@/components/ui/input"; |
9 | 8 | import { Label } from "@/components/ui/label"; |
10 | 9 | import { type Organization, useOrganizations } from "@/hooks/use-organizations"; |
@@ -32,130 +31,123 @@ export function GeneralSettings({ organization }: GeneralSettingsProps) { |
32 | 31 | setSlug(cleanSlug(value)); |
33 | 32 | }; |
34 | 33 |
|
| 34 | + const hasChanges = name !== organization.name || slug !== organization.slug; |
| 35 | + |
35 | 36 | const handleSave = async () => { |
36 | | - if (!(name.trim() && slug.trim())) { |
37 | | - toast.error("Name and slug are required"); |
| 37 | + if (!name.trim()) { |
| 38 | + toast.error("Name is required"); |
| 39 | + return; |
| 40 | + } |
| 41 | + if (!slug.trim()) { |
| 42 | + toast.error("Slug is required"); |
38 | 43 | return; |
39 | 44 | } |
40 | 45 |
|
41 | 46 | setIsSaving(true); |
42 | 47 | try { |
43 | 48 | await updateOrganizationAsync({ |
44 | 49 | organizationId: organization.id, |
45 | | - data: { |
46 | | - name: name.trim(), |
47 | | - slug: slug.trim(), |
48 | | - }, |
| 50 | + data: { name: name.trim(), slug: slug.trim() }, |
49 | 51 | }); |
50 | | - |
51 | | - toast.success("Organization updated successfully"); |
52 | | - |
53 | | - // If slug changed, we might need to update the URL context |
54 | | - // but since we're using active organization, this should be handled automatically |
55 | | - } catch (_error) { |
56 | | - toast.error("Failed to update organization"); |
| 52 | + toast.success("Settings updated"); |
| 53 | + } catch { |
| 54 | + toast.error("Failed to update settings"); |
57 | 55 | } finally { |
58 | 56 | setIsSaving(false); |
59 | 57 | } |
60 | 58 | }; |
61 | 59 |
|
62 | | - const hasChanges = name !== organization.name || slug !== organization.slug; |
63 | | - |
64 | 60 | return ( |
65 | | - <div className="h-full p-4 sm:p-6"> |
66 | | - <div className="space-y-6 sm:space-y-8"> |
67 | | - {/* Content Sections */} |
68 | | - <div className="space-y-6 sm:space-y-8"> |
69 | | - {/* Logo Upload Section */} |
70 | | - <div className="rounded border bg-card p-4 sm:p-6"> |
71 | | - <div className="space-y-3 sm:space-y-4"> |
72 | | - <div> |
73 | | - <h3 className="font-semibold text-base sm:text-lg"> |
74 | | - Organization Logo |
75 | | - </h3> |
76 | | - <p className="text-muted-foreground text-xs sm:text-sm"> |
77 | | - Upload a logo to represent your organization |
78 | | - </p> |
79 | | - </div> |
80 | | - <OrganizationLogoUploader organization={organization} /> |
| 61 | + <div className="h-full lg:grid lg:grid-cols-[1fr_18rem]"> |
| 62 | + {/* Main Content */} |
| 63 | + <div className="flex flex-col border-b lg:border-b-0 lg:border-r"> |
| 64 | + <div className="flex-1 space-y-6 p-5"> |
| 65 | + {/* Logo Section */} |
| 66 | + <OrganizationLogoUploader organization={organization} /> |
| 67 | + |
| 68 | + {/* Name & Slug */} |
| 69 | + <div className="grid gap-4 sm:grid-cols-2"> |
| 70 | + <div className="space-y-2"> |
| 71 | + <Label htmlFor="name">Name</Label> |
| 72 | + <Input |
| 73 | + id="name" |
| 74 | + onChange={(e) => setName(e.target.value)} |
| 75 | + placeholder="Organization name…" |
| 76 | + value={name} |
| 77 | + /> |
| 78 | + </div> |
| 79 | + <div className="space-y-2"> |
| 80 | + <Label htmlFor="slug">Slug</Label> |
| 81 | + <Input |
| 82 | + id="slug" |
| 83 | + onChange={(e) => handleSlugChange(e.target.value)} |
| 84 | + placeholder="organization-slug…" |
| 85 | + value={slug} |
| 86 | + /> |
| 87 | + <p className="text-muted-foreground text-xs"> |
| 88 | + Used in URLs: /{slug} |
| 89 | + </p> |
81 | 90 | </div> |
82 | 91 | </div> |
| 92 | + </div> |
83 | 93 |
|
84 | | - {/* Name and Slug Section */} |
85 | | - <div className="rounded border bg-card p-4 sm:p-6"> |
86 | | - <div className="space-y-4 sm:space-y-6"> |
87 | | - <div> |
88 | | - <h3 className="font-semibold text-base sm:text-lg"> |
89 | | - Basic Information |
90 | | - </h3> |
91 | | - <p className="text-muted-foreground text-xs sm:text-sm"> |
92 | | - Configure your organization's name and URL identifier |
93 | | - </p> |
94 | | - </div> |
95 | | - |
96 | | - <div className="grid gap-4 sm:grid-cols-2 sm:gap-6"> |
97 | | - <div className="space-y-2 sm:space-y-3"> |
98 | | - <Label |
99 | | - className="font-medium text-xs sm:text-sm" |
100 | | - htmlFor="name" |
101 | | - > |
102 | | - Organization Name |
103 | | - </Label> |
104 | | - <Input |
105 | | - id="name" |
106 | | - onChange={(e) => setName(e.target.value)} |
107 | | - placeholder="Enter organization name" |
108 | | - value={name} |
109 | | - /> |
110 | | - </div> |
111 | | - <div className="space-y-2 sm:space-y-3"> |
112 | | - <Label |
113 | | - className="font-medium text-xs sm:text-sm" |
114 | | - htmlFor="slug" |
115 | | - > |
116 | | - Organization Slug |
117 | | - </Label> |
118 | | - <Input |
119 | | - id="slug" |
120 | | - onChange={(e) => handleSlugChange(e.target.value)} |
121 | | - placeholder="organization-slug" |
122 | | - value={slug} |
123 | | - /> |
124 | | - <p className="text-muted-foreground text-xs"> |
125 | | - This will be used in your organization URL |
126 | | - </p> |
127 | | - </div> |
128 | | - </div> |
129 | | - |
130 | | - {/* Save Button */} |
131 | | - {hasChanges && ( |
132 | | - <div className="flex justify-end border-t pt-3 sm:pt-4"> |
133 | | - <Button |
134 | | - className="px-4 text-xs sm:px-6 sm:text-sm" |
135 | | - disabled={isSaving} |
136 | | - onClick={handleSave} |
137 | | - > |
138 | | - {isSaving ? ( |
139 | | - <> |
140 | | - <div className="mr-2 h-3 w-3 animate-spin rounded-full border border-primary-foreground/30 border-t-primary-foreground sm:h-4 sm:w-4" /> |
141 | | - Saving... |
142 | | - </> |
143 | | - ) : ( |
144 | | - <> |
145 | | - <FloppyDiskIcon |
146 | | - className="mr-2 h-3 w-3 sm:h-4 sm:w-4" |
147 | | - size={12} |
148 | | - /> |
149 | | - Save Changes |
150 | | - </> |
151 | | - )} |
152 | | - </Button> |
153 | | - </div> |
| 94 | + {/* Save Footer */} |
| 95 | + {hasChanges && ( |
| 96 | + <div className="flex items-center justify-between border-t bg-muted/30 px-5 py-3"> |
| 97 | + <p className="text-muted-foreground text-sm">You have unsaved changes</p> |
| 98 | + <Button disabled={isSaving} onClick={handleSave} size="sm"> |
| 99 | + {isSaving ? ( |
| 100 | + <> |
| 101 | + <div className="mr-2 h-3 w-3 animate-spin rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground" /> |
| 102 | + Saving… |
| 103 | + </> |
| 104 | + ) : ( |
| 105 | + <> |
| 106 | + <FloppyDiskIcon className="mr-2" size={14} /> |
| 107 | + Save Changes |
| 108 | + </> |
154 | 109 | )} |
155 | | - </div> |
| 110 | + </Button> |
156 | 111 | </div> |
157 | | - </div> |
| 112 | + )} |
158 | 113 | </div> |
| 114 | + |
| 115 | + {/* Sidebar */} |
| 116 | + <aside className="flex flex-col gap-4 bg-muted/30 p-5"> |
| 117 | + {/* Org Info Card */} |
| 118 | + <div className="flex items-center gap-3 rounded border bg-background p-4"> |
| 119 | + <div className="flex h-10 w-10 items-center justify-center rounded bg-primary/10"> |
| 120 | + <BuildingsIcon className="text-primary" size={20} weight="duotone" /> |
| 121 | + </div> |
| 122 | + <div className="min-w-0"> |
| 123 | + <p className="truncate font-semibold">{organization.name}</p> |
| 124 | + <p className="truncate text-muted-foreground text-sm"> |
| 125 | + /{organization.slug} |
| 126 | + </p> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + |
| 130 | + {/* Docs Link */} |
| 131 | + <Button asChild className="w-full justify-start" variant="outline"> |
| 132 | + <a |
| 133 | + href="https://www.databuddy.cc/docs/getting-started" |
| 134 | + rel="noopener noreferrer" |
| 135 | + target="_blank" |
| 136 | + > |
| 137 | + <BookOpenIcon className="mr-2" size={16} /> |
| 138 | + Documentation |
| 139 | + </a> |
| 140 | + </Button> |
| 141 | + |
| 142 | + {/* Tip */} |
| 143 | + <div className="mt-auto rounded border border-dashed bg-background/50 p-4"> |
| 144 | + <p className="mb-2 font-medium text-sm">Quick tip</p> |
| 145 | + <p className="text-muted-foreground text-xs leading-relaxed"> |
| 146 | + The slug is used in URLs and API requests. Keep it short and |
| 147 | + memorable. |
| 148 | + </p> |
| 149 | + </div> |
| 150 | + </aside> |
159 | 151 | </div> |
160 | 152 | ); |
161 | 153 | } |
0 commit comments