@@ -27,6 +27,37 @@ <h1 class="text-3xl font-bold tracking-tight">My Account</h1>
2727 </ section >
2828 </ main >
2929
30+ < template id ="emptyStateTemplate ">
31+ < div class ="rounded-lg border border-dashed border-slate-300 p-4 text-slate-600 "> No workspaces found for this account.</ div >
32+ </ template >
33+
34+ < template id ="workspaceTemplate ">
35+ < section class ="rounded-xl border border-slate-200 p-4 ">
36+ < div class ="flex flex-wrap items-start justify-between gap-3 ">
37+ < div >
38+ < div class ="inline-flex items-center gap-2 ">
39+ < div class ="text-lg font-semibold " data-workspace-name > </ div >
40+ < button type ="button " class ="inline-flex items-center rounded-md border border-slate-300 bg-white p-1 text-slate-700 hover:bg-slate-50 " data-edit-name aria-label ="Edit workspace name ">
41+ < svg xmlns ="http://www.w3.org/2000/svg " viewBox ="0 0 24 24 " fill ="currentColor " class ="h-4 w-4 "> < path d ="M3 17.25V21h3.75L17.8 9.94l-3.75-3.75L3 17.25Zm18-11.5a1 1 0 0 0 0-1.41l-1.34-1.34a1 1 0 0 0-1.41 0l-1.18 1.18 3.75 3.75L21 5.75Z "/> </ svg >
42+ </ button >
43+ </ div >
44+ < div class ="text-sm text-slate-600 "> Workspace ID: < span data-workspace-id-label > </ span > </ div >
45+ </ div >
46+ < div class ="flex flex-wrap gap-2 text-xs ">
47+ < span class ="rounded-full border border-slate-300 px-2 py-1 text-slate-700 "> Tier: < span data-tier-label > </ span > </ span >
48+ < span class ="rounded-full border border-slate-300 px-2 py-1 text-slate-700 "> Roles: < span data-roles-label > </ span > </ span >
49+ </ div >
50+ </ div >
51+ < div class ="mt-3 flex flex-wrap items-center gap-2 ">
52+ < code class ="max-w-full break-all rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs sm:text-sm " data-key-view > </ code >
53+ < button type ="button " class ="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50 " data-toggle-key data-visible ="false " aria-label ="Show API key ">
54+ < svg xmlns ="http://www.w3.org/2000/svg " viewBox ="0 0 24 24 " fill ="currentColor " class ="h-4 w-4 "> < path d ="M12 5c-5.5 0-9.5 4.5-10.8 6.2a1.3 1.3 0 0 0 0 1.6C2.5 14.5 6.5 19 12 19s9.5-4.5 10.8-6.2a1.3 1.3 0 0 0 0-1.6C21.5 9.5 17.5 5 12 5Zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z "/> </ svg >
55+ </ button >
56+ < button type ="button " class ="rounded-lg bg-[#880000] px-3 py-2 text-xs font-semibold text-white transition hover:bg-[#6d0000] " data-copy-key > Copy API Key</ button >
57+ </ div >
58+ </ section >
59+ </ template >
60+
3061 < script >
3162 ( function ( ) {
3263 const ACCESS_TOKEN_KEY = '_mongooseStudioAccessToken' ;
@@ -69,77 +100,117 @@ <h1 class="text-3xl font-bold tracking-tight">My Account</h1>
69100 const workspaces = data . workspaces || [ ] ;
70101
71102 userSummary . textContent = user . email ? user . email : 'Signed in' ;
103+ workspaceList . innerHTML = '' ;
72104
73105 if ( workspaces . length === 0 ) {
74- workspaceList . innerHTML = '<div class="rounded-lg border border-dashed border-slate-300 p-4 text-slate-600">No workspaces found for this account.</div>' ;
106+ workspaceList . appendChild ( createElementFromTemplate ( 'emptyStateTemplate' ) ) ;
75107 return ;
76108 }
77109
78- workspaceList . innerHTML = '' ;
79110 for ( const workspace of workspaces ) {
80- const item = document . createElement ( 'section ') ;
81- item . className = 'rounded-xl border border-slate-200 p-4' ;
111+ const item = createElementFromTemplate ( 'workspaceTemplate ') ;
112+ item . setAttribute ( 'data-workspace-id' , String ( workspace . _id ) ) ;
82113
83114 const roles = Array . isArray ( workspace . roles ) && workspace . roles . length ? workspace . roles . join ( ', ' ) : 'none' ;
84115 const apiKey = String ( workspace . apiKey || '' ) ;
85116 const maskedApiKey = apiKey ? '\u2022' . repeat ( Math . max ( 8 , apiKey . length ) ) : '' ;
117+ const canEditName = Array . isArray ( workspace . roles ) && ( workspace . roles . includes ( 'owner' ) || workspace . roles . includes ( 'admin' ) ) ;
118+
119+ item . querySelector ( '[data-workspace-name]' ) . textContent = workspace . name || 'Unnamed Workspace' ;
120+ item . querySelector ( '[data-workspace-id-label]' ) . textContent = String ( workspace . _id ) ;
121+ item . querySelector ( '[data-tier-label]' ) . textContent = workspace . subscriptionTier || 'unknown' ;
122+ item . querySelector ( '[data-roles-label]' ) . textContent = roles ;
123+
124+ const editButton = item . querySelector ( '[data-edit-name]' ) ;
125+ if ( ! canEditName ) {
126+ editButton . remove ( ) ;
127+ }
128+
129+ const keyView = item . querySelector ( '[data-key-view]' ) ;
130+ keyView . textContent = maskedApiKey ;
86131
87- item . innerHTML = [
88- '<div class="flex flex-wrap items-start justify-between gap-3">' ,
89- '<div>' ,
90- '<div class="text-lg font-semibold">' + escapeHtml ( workspace . name || 'Unnamed Workspace' ) + '</div>' ,
91- '<div class="text-sm text-slate-600">Workspace ID: ' + escapeHtml ( String ( workspace . _id ) ) + '</div>' ,
92- '</div>' ,
93- '<div class="flex flex-wrap gap-2 text-xs">' ,
94- '<span class="rounded-full border border-slate-300 px-2 py-1 text-slate-700">Tier: ' + escapeHtml ( workspace . subscriptionTier || 'unknown' ) + '</span>' ,
95- '<span class="rounded-full border border-slate-300 px-2 py-1 text-slate-700">Roles: ' + escapeHtml ( roles ) + '</span>' ,
96- '</div>' ,
97- '</div>' ,
98- '<div class="mt-3 flex flex-wrap items-center gap-2">' ,
99- '<code class="max-w-full break-all rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs sm:text-sm" data-key-view>' + escapeHtml ( maskedApiKey ) + '</code>' ,
100- '<button type="button" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50" data-toggle-key data-key="' + escapeHtml ( apiKey ) + '" data-masked="' + escapeHtml ( maskedApiKey ) + '" data-visible="false" aria-label="Show API key">' ,
101- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M12 5c-5.5 0-9.5 4.5-10.8 6.2a1.3 1.3 0 0 0 0 1.6C2.5 14.5 6.5 19 12 19s9.5-4.5 10.8-6.2a1.3 1.3 0 0 0 0-1.6C21.5 9.5 17.5 5 12 5Zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/></svg>' ,
102- '</button>' ,
103- '<button type="button" class="rounded-lg bg-[#880000] px-3 py-2 text-xs font-semibold text-white transition hover:bg-[#6d0000]" data-copy-key="' + escapeHtml ( apiKey ) + '">Copy API Key</button>' ,
104- '</div>'
105- ] . join ( '' ) ;
132+ const toggleButton = item . querySelector ( '[data-toggle-key]' ) ;
133+ toggleButton . setAttribute ( 'data-key' , apiKey ) ;
134+ toggleButton . setAttribute ( 'data-masked' , maskedApiKey ) ;
135+
136+ const copyButton = item . querySelector ( '[data-copy-key]' ) ;
137+ copyButton . setAttribute ( 'data-copy-key' , apiKey ) ;
106138
107139 workspaceList . appendChild ( item ) ;
108- }
109140
110- workspaceList . querySelectorAll ( 'button[data-copy-key]' ) . forEach ( function ( button ) {
111- button . addEventListener ( 'click' , async function ( ) {
112- const key = button . getAttribute ( 'data-copy-key' ) || '' ;
141+ copyButton . addEventListener ( 'click' , async function ( ) {
142+ const key = copyButton . getAttribute ( 'data-copy-key' ) || '' ;
113143 if ( ! key ) {
114144 return ;
115145 }
116146 await navigator . clipboard . writeText ( key ) ;
117- const prev = button . textContent ;
118- button . textContent = 'Copied' ;
147+ const prev = copyButton . textContent ;
148+ copyButton . textContent = 'Copied' ;
119149 setTimeout ( function ( ) {
120- button . textContent = prev ;
150+ copyButton . textContent = prev ;
121151 } , 1200 ) ;
122152 } ) ;
123- } ) ;
124153
125- workspaceList . querySelectorAll ( 'button[data-toggle-key]' ) . forEach ( function ( button ) {
126- button . addEventListener ( 'click' , function ( ) {
127- const visible = button . getAttribute ( 'data-visible' ) === 'true' ;
128- const code = button . parentElement . querySelector ( '[data-key-view]' ) ;
129- if ( ! code ) {
154+ toggleButton . addEventListener ( 'click' , function ( ) {
155+ const visible = toggleButton . getAttribute ( 'data-visible' ) === 'true' ;
156+ if ( ! keyView ) {
130157 return ;
131158 }
132159 if ( visible ) {
133- code . textContent = button . getAttribute ( 'data-masked' ) || '' ;
134- button . setAttribute ( 'data-visible' , 'false' ) ;
135- button . setAttribute ( 'aria-label' , 'Show API key' ) ;
160+ keyView . textContent = toggleButton . getAttribute ( 'data-masked' ) || '' ;
161+ toggleButton . setAttribute ( 'data-visible' , 'false' ) ;
162+ toggleButton . setAttribute ( 'aria-label' , 'Show API key' ) ;
136163 } else {
137- code . textContent = button . getAttribute ( 'data-key' ) || '' ;
138- button . setAttribute ( 'data-visible' , 'true' ) ;
139- button . setAttribute ( 'aria-label' , 'Hide API key' ) ;
164+ keyView . textContent = toggleButton . getAttribute ( 'data-key' ) || '' ;
165+ toggleButton . setAttribute ( 'data-visible' , 'true' ) ;
166+ toggleButton . setAttribute ( 'aria-label' , 'Hide API key' ) ;
140167 }
141168 } ) ;
142- } ) ;
169+
170+ if ( editButton ) {
171+ editButton . addEventListener ( 'click' , async function ( ) {
172+ const section = editButton . closest ( 'section' ) ;
173+ if ( ! section ) {
174+ return ;
175+ }
176+ const workspaceId = section . getAttribute ( 'data-workspace-id' ) ;
177+ const nameEl = section . querySelector ( '[data-workspace-name]' ) ;
178+ if ( ! workspaceId || ! nameEl ) {
179+ return ;
180+ }
181+ const currentName = nameEl . textContent || '' ;
182+ const nextName = window . prompt ( 'Update workspace name' , currentName ) ;
183+ if ( nextName == null || nextName . trim ( ) === '' || nextName . trim ( ) === currentName ) {
184+ return ;
185+ }
186+
187+ try {
188+ const token = localStorage . getItem ( ACCESS_TOKEN_KEY ) ;
189+ const { workspace : updatedWorkspace } = await postJson ( '/.netlify/functions/updateWorkspace' , {
190+ workspaceId : workspaceId . trim ( ) ,
191+ name : nextName . trim ( )
192+ } , token ) ;
193+ nameEl . textContent = updatedWorkspace . name || nextName . trim ( ) ;
194+ } catch ( err ) {
195+ showError ( err . message || 'Failed to update workspace name' ) ;
196+ }
197+ } ) ;
198+ }
199+ }
200+ }
201+
202+ function createElementFromTemplate ( templateId ) {
203+ const template = document . getElementById ( templateId ) ;
204+ if ( ! template ) {
205+ throw new Error ( 'Missing template: ' + templateId ) ;
206+ }
207+ const container = document . createElement ( 'div' ) ;
208+ container . innerHTML = template . innerHTML . trim ( ) ;
209+ const element = container . firstElementChild ;
210+ if ( ! element ) {
211+ throw new Error ( 'Template produced no element: ' + templateId ) ;
212+ }
213+ return element ;
143214 }
144215
145216 async function postJson ( url , body , token ) {
@@ -174,15 +245,6 @@ <h1 class="text-3xl font-bold tracking-tight">My Account</h1>
174245 function showError ( message ) {
175246 errorEl . textContent = message ;
176247 }
177-
178- function escapeHtml ( str ) {
179- return String ( str )
180- . replaceAll ( '&' , '&' )
181- . replaceAll ( '<' , '<' )
182- . replaceAll ( '>' , '>' )
183- . replaceAll ( '"' , '"' )
184- . replaceAll ( "'" , ''' ) ;
185- }
186248 } ) ( ) ;
187249 </ script >
188250 </ body >
0 commit comments