Skip to content

Commit 03291fe

Browse files
committed
match profile pane, fix checkbox
1 parent 2ef06a6 commit 03291fe

5 files changed

Lines changed: 582 additions & 153 deletions

File tree

src/social/editProfileDetails.ts

Lines changed: 150 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LiveStore, NamedNode, st, sym } from 'rdflib'
22
import { ns } from 'solid-ui'
3-
import { closeIcon, editIcon } from './icons'
3+
import 'solid-ui/components/actions/button'
4+
import { closeIcon, editIcon, plusIcon, trashIcon } from './icons'
45
/* The following code was generated by AI Model: GPT-5.4
56
Prompt: can you create me a form to enter this data [
67
ns.foaf('homepage'),
@@ -19,7 +20,7 @@ type ProfileLinkField = {
1920
predicate: NamedNode
2021
}
2122

22-
type ProfileLinksValues = Record<ProfileLinkField['key'], string>
23+
type ProfileLinksValues = Record<ProfileLinkField['key'], string[]>
2324

2425
type EditProfileDetailsButtonOptions = {
2526
dom: HTMLDocument
@@ -56,6 +57,15 @@ export const profileLinkFields: ProfileLinkField[] = [
5657
}
5758
]
5859

60+
function createEmptyProfileLinkValues (): ProfileLinksValues {
61+
return {
62+
homepage: [''],
63+
weblog: [''],
64+
workplaceHomepage: [''],
65+
schoolHomepage: ['']
66+
}
67+
}
68+
5969
function readProfileLinkValues (store: LiveStore, subject: NamedNode): ProfileLinksValues {
6070
const doc = subject.doc()
6171
return profileLinkFields.reduce((result, field) => {
@@ -64,20 +74,14 @@ function readProfileLinkValues (store: LiveStore, subject: NamedNode): ProfileLi
6474
.map((statement) => statement.object?.value || '')
6575
.filter(Boolean)
6676

67-
result[field.key] = values.join('\n')
77+
result[field.key] = values.length ? values : ['']
6878
return result
69-
}, {
70-
homepage: '',
71-
weblog: '',
72-
workplaceHomepage: '',
73-
schoolHomepage: ''
74-
} as ProfileLinksValues)
79+
}, createEmptyProfileLinkValues())
7580
}
7681

77-
function normalizeUrlList (value: string, fieldLabel: string): string[] {
82+
function normalizeUrlList (values: string[], fieldLabel: string): string[] {
7883
const unique = new Set<string>()
79-
const lines = value
80-
.split('\n')
84+
const lines = values
8185
.map((entry) => entry.trim())
8286
.filter(Boolean)
8387

@@ -99,6 +103,14 @@ function normalizeUrlList (value: string, fieldLabel: string): string[] {
99103
return [...unique]
100104
}
101105

106+
function formatDisplayUrl (value: string): string {
107+
let displayValue = value.replace(/^https?:\/\//i, '')
108+
if (/^[^/?#]+\/$/.test(displayValue)) {
109+
displayValue = displayValue.slice(0, -1)
110+
}
111+
return displayValue
112+
}
113+
102114
async function saveProfileLinkValues (
103115
store: LiveStore,
104116
subject: NamedNode,
@@ -163,25 +175,10 @@ export function appendProfileLinks (
163175
for (const uri of uris) {
164176
if (uri === previousUri) continue
165177
previousUri = uri
166-
167-
let label = field.label
168-
if (uris.length > 1) {
169-
const schemeIndex = uri.indexOf('//')
170-
if (schemeIndex > 0) {
171-
let end = uri.indexOf('/', schemeIndex + 2)
172-
const dottedEnd = uri.lastIndexOf('.', end)
173-
if (dottedEnd > 0) end = dottedEnd
174-
const hostLabel = uri.slice(schemeIndex + 2, end)
175-
if (hostLabel) {
176-
label = `${hostLabel} ${label}`
177-
}
178-
}
179-
}
180-
181-
const textNode = dom.createTextNode(label)
182178
const anchor = dom.createElement('a')
183-
anchor.appendChild(textNode)
179+
anchor.textContent = formatDisplayUrl(uri)
184180
anchor.setAttribute('href', uri)
181+
anchor.setAttribute('title', `${field.label}: ${uri}`)
185182

186183
const item = dom.createElement('div')
187184
item.className = 'social-pane__link-button'
@@ -214,6 +211,10 @@ export function createEditProfileDetailsButton (
214211

215212
const openDialog = () => {
216213
const initialValues = readProfileLinkValues(store, subject)
214+
const formState = profileLinkFields.reduce((result, field) => {
215+
result[field.key] = [...initialValues[field.key]]
216+
return result
217+
}, createEmptyProfileLinkValues())
217218
const overlay = dom.createElement('div')
218219
overlay.classList.add('social-pane__dialog-backdrop', 'flex-center')
219220

@@ -233,57 +234,147 @@ export function createEditProfileDetailsButton (
233234
title.textContent = 'Edit profile links'
234235
dialogHeader.appendChild(title)
235236

236-
const closeIconButton = dom.createElement('button')
237+
const closeIconButton = dom.createElement('solid-ui-button')
237238
closeIconButton.type = 'button'
238239
closeIconButton.className = 'social-pane__dialog-close'
239240
closeIconButton.setAttribute('aria-label', 'Close dialog')
240-
closeIconButton.innerHTML = closeIcon
241+
closeIconButton.setAttribute('title', 'Close')
242+
closeIconButton.variant = 'icon'
243+
closeIconButton.size = 'md'
244+
const closeIconButtonIcon = dom.createElement('span')
245+
closeIconButtonIcon.className = 'social-pane__dialog-close-icon'
246+
closeIconButtonIcon.setAttribute('slot', 'icon')
247+
closeIconButtonIcon.setAttribute('aria-hidden', 'true')
248+
closeIconButtonIcon.innerHTML = closeIcon
249+
closeIconButton.appendChild(closeIconButtonIcon)
241250
dialogHeader.appendChild(closeIconButton)
242251

243252
const form = dom.createElement('form')
244253
form.classList.add('social-pane__dialog-form', 'flex-column')
245254
dialog.appendChild(form)
246255

256+
const fieldsContainer = dom.createElement('div')
257+
fieldsContainer.className = 'social-pane__dialog-fields'
258+
form.appendChild(fieldsContainer)
259+
247260
const errorMessage = dom.createElement('p')
248261
errorMessage.className = 'social-pane__dialog-error'
249262

250-
const fields = new Map<ProfileLinkField['key'], HTMLInputElement>()
251-
for (const field of profileLinkFields) {
252-
const fieldWrapper = dom.createElement('label')
253-
fieldWrapper.classList.add('social-pane__dialog-field', 'flex-column')
254-
255-
const fieldLabel = dom.createElement('span')
256-
fieldLabel.className = 'social-pane__dialog-label'
257-
fieldLabel.textContent = field.label
258-
fieldWrapper.appendChild(fieldLabel)
259-
260-
const input = dom.createElement('input')
261-
input.classList.add('social-pane__dialog-input', 'input')
262-
input.type = 'url'
263-
input.placeholder = field.placeholder
264-
input.value = initialValues[field.key].split('\n')[0] || ''
265-
fieldWrapper.appendChild(input)
266-
267-
fields.set(field.key, input)
268-
form.appendChild(fieldWrapper)
263+
const focusRow = (fieldKey: ProfileLinkField['key'], rowIndex: number) => {
264+
const nextField = fieldsContainer.querySelector<HTMLInputElement>(`input[data-profile-link-key="${fieldKey}"][data-profile-link-index="${String(rowIndex)}"]`)
265+
nextField?.focus()
266+
}
267+
268+
const renderFields = (focusTarget?: { fieldKey: ProfileLinkField['key'], rowIndex: number }) => {
269+
fieldsContainer.replaceChildren()
270+
271+
for (const field of profileLinkFields) {
272+
const fieldWrapper = dom.createElement('section')
273+
fieldWrapper.classList.add('social-pane__dialog-field', 'flex-column')
274+
275+
const fieldHeader = dom.createElement('div')
276+
fieldHeader.className = 'social-pane__dialog-field-header'
277+
fieldWrapper.appendChild(fieldHeader)
278+
279+
const fieldLabel = dom.createElement('h3')
280+
fieldLabel.className = 'social-pane__dialog-label'
281+
fieldLabel.textContent = field.label
282+
fieldHeader.appendChild(fieldLabel)
283+
284+
const addButton = dom.createElement('button')
285+
addButton.type = 'button'
286+
addButton.className = 'social-pane__dialog-row-button social-pane__dialog-row-button--add'
287+
addButton.setAttribute('aria-label', `Add another ${field.label.toLowerCase()}`)
288+
const addButtonIcon = dom.createElement('span')
289+
addButtonIcon.className = 'social-pane__dialog-row-button-icon social-pane__dialog-row-button-icon--add'
290+
addButtonIcon.setAttribute('aria-hidden', 'true')
291+
addButtonIcon.innerHTML = plusIcon
292+
addButton.appendChild(addButtonIcon)
293+
addButton.appendChild(dom.createTextNode('Add More'))
294+
addButton.addEventListener('click', () => {
295+
formState[field.key].push('')
296+
renderFields({ fieldKey: field.key, rowIndex: formState[field.key].length - 1 })
297+
})
298+
fieldHeader.appendChild(addButton)
299+
300+
const rows = dom.createElement('div')
301+
rows.className = 'social-pane__dialog-row-list'
302+
fieldWrapper.appendChild(rows)
303+
304+
const values = formState[field.key].length ? formState[field.key] : ['']
305+
values.forEach((value, index) => {
306+
const row = dom.createElement('div')
307+
row.className = 'social-pane__dialog-row'
308+
309+
const input = dom.createElement('input')
310+
input.classList.add('social-pane__dialog-input', 'input')
311+
input.type = 'url'
312+
input.placeholder = field.placeholder
313+
input.value = value
314+
input.setAttribute('data-profile-link-key', field.key)
315+
input.setAttribute('data-profile-link-index', String(index))
316+
input.addEventListener('input', () => {
317+
formState[field.key][index] = input.value
318+
})
319+
row.appendChild(input)
320+
321+
const removeButton = dom.createElement('solid-ui-button')
322+
removeButton.type = 'button'
323+
removeButton.className = 'social-pane__dialog-row-button--remove'
324+
removeButton.setAttribute('aria-label', `Remove ${field.label.toLowerCase()} ${index + 1}`)
325+
removeButton.setAttribute('title', 'Remove')
326+
removeButton.variant = 'icon'
327+
removeButton.size = 'sm'
328+
const removeButtonIcon = dom.createElement('span')
329+
removeButtonIcon.className = 'social-pane__dialog-row-button-icon social-pane__dialog-row-button-icon--remove'
330+
removeButtonIcon.setAttribute('slot', 'icon')
331+
removeButtonIcon.setAttribute('aria-hidden', 'true')
332+
removeButtonIcon.innerHTML = trashIcon
333+
removeButton.appendChild(removeButtonIcon)
334+
removeButton.addEventListener('click', () => {
335+
if (formState[field.key].length === 1) {
336+
formState[field.key][0] = ''
337+
} else {
338+
formState[field.key].splice(index, 1)
339+
}
340+
renderFields()
341+
})
342+
row.appendChild(removeButton)
343+
344+
rows.appendChild(row)
345+
})
346+
347+
fieldsContainer.appendChild(fieldWrapper)
348+
}
349+
350+
if (focusTarget) {
351+
focusRow(focusTarget.fieldKey, focusTarget.rowIndex)
352+
}
269353
}
270354

355+
renderFields()
356+
271357
form.appendChild(errorMessage)
272358

273359
const actions = dom.createElement('div')
274360
actions.className = 'social-pane__dialog-actions'
275361
form.appendChild(actions)
276362

277-
const closeButton = dom.createElement('button')
363+
const closeButton = dom.createElement('solid-ui-button')
278364
closeButton.type = 'button'
279-
closeButton.className = 'social-pane__dialog-button btn-light'
280-
closeButton.textContent = 'Close'
365+
closeButton.className = 'social-pane__dialog-button--cancel'
366+
closeButton.setAttribute('aria-label', 'Close dialog')
367+
closeButton.label = 'Close'
368+
closeButton.variant = 'secondary'
369+
closeButton.size = 'md'
281370
actions.appendChild(closeButton)
282371

283-
const saveButton = dom.createElement('button')
372+
const saveButton = dom.createElement('solid-ui-button')
284373
saveButton.type = 'submit'
285-
saveButton.className = 'social-pane__dialog-button btn-primary'
286-
saveButton.textContent = 'Save Changes'
374+
saveButton.className = 'social-pane__dialog-button--save'
375+
saveButton.label = 'Save Changes'
376+
saveButton.variant = 'primary'
377+
saveButton.size = 'md'
287378
actions.appendChild(saveButton)
288379

289380
const closeDialog = () => {
@@ -310,14 +401,9 @@ export function createEditProfileDetailsButton (
310401
closeIconButton.disabled = true
311402

312403
const nextValues = profileLinkFields.reduce((result, field) => {
313-
result[field.key] = fields.get(field.key)?.value || ''
404+
result[field.key] = [...formState[field.key]]
314405
return result
315-
}, {
316-
homepage: '',
317-
weblog: '',
318-
workplaceHomepage: '',
319-
schoolHomepage: ''
320-
} as ProfileLinksValues)
406+
}, createEmptyProfileLinkValues())
321407

322408
try {
323409
await saveProfileLinkValues(store, subject, nextValues)
@@ -333,7 +419,7 @@ export function createEditProfileDetailsButton (
333419
})
334420

335421
dom.body.appendChild(overlay)
336-
fields.get('homepage')?.focus()
422+
focusRow('homepage', 0)
337423
}
338424

339425
button.addEventListener('click', openDialog)

src/social/icons.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,38 @@ export const closeIcon = html`
5353
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5301 0.244005C10.8555 -0.0813835 11.3834 -0.0812864 11.7088 0.244005C12.0338 0.569352 12.0338 1.09639 11.7088 1.42174L7.15416 5.97545L11.7088 10.5301C12.0341 10.8556 12.0342 11.3835 11.7088 11.7088C11.3835 12.0342 10.8556 12.0341 10.5301 11.7088L5.97545 7.15416L1.42174 11.7088C1.09639 12.0338 0.569351 12.0338 0.244004 11.7088C-0.0812866 11.3834 -0.0813828 10.8555 0.244004 10.5301L4.79772 5.97545L0.244004 1.42174C-0.0812866 1.09629 -0.0813828 0.569393 0.244004 0.244005C0.569393 -0.0813835 1.09629 -0.0812864 1.42174 0.244005L5.97545 4.79772L10.5301 0.244005Z" fill="#4A5565"/>
5454
</svg>
5555
`
56+
export const trashIcon = html`
57+
<svg xmlns="http://www.w3.org/2000/svg"
58+
width="20"
59+
height="20"
60+
viewBox="0 0 20 20"
61+
fill="none"
62+
aria-hidden="true"
63+
focusable="false"
64+
>
65+
<path d="M8.33301 9.16675V14.1667" stroke="#D1D5DC" stroke-width="1.45833" stroke-linecap="round" stroke-linejoin="round"/>
66+
<path d="M11.667 9.16675V14.1667" stroke="#D1D5DC" stroke-width="1.45833" stroke-linecap="round" stroke-linejoin="round"/>
67+
<path d="M15.8337 5V16.6667C15.8337 17.1087 15.6581 17.5326 15.3455 17.8452C15.0329 18.1577 14.609 18.3333 14.167 18.3333H5.83366C5.39163 18.3333 4.96771 18.1577 4.65515 17.8452C4.34259 17.5326 4.16699 17.1087 4.16699 16.6667V5" stroke="#D1D5DC" stroke-width="1.45833" stroke-linecap="round" stroke-linejoin="round"/>
68+
<path d="M2.5 5H17.5" stroke="#D1D5DC" stroke-width="1.45833" stroke-linecap="round" stroke-linejoin="round"/>
69+
<path d="M6.66699 5.00008V3.33341C6.66699 2.89139 6.84259 2.46746 7.15515 2.1549C7.46771 1.84234 7.89163 1.66675 8.33366 1.66675H11.667C12.109 1.66675 12.5329 1.84234 12.8455 2.1549C13.1581 2.46746 13.3337 2.89139 13.3337 3.33341V5.00008" stroke="#D1D5DC" stroke-width="1.45833" stroke-linecap="round" stroke-linejoin="round"/>
70+
</svg>
71+
`
72+
export const plusIcon = html`
73+
<svg xmlns="http://www.w3.org/2000/svg"
74+
width="12"
75+
height="12"
76+
viewBox="0 0 12 12"
77+
fill="none"
78+
aria-hidden="true"
79+
focusable="false"
80+
>
81+
<g clip-path="url(#clip0_3001_2564)">
82+
<path d="M6.06055 0.10083C6.53372 0.101056 6.91793 0.485037 6.91797 0.958252V5.20532H11.165C11.6384 5.20532 12.0225 5.58936 12.0225 6.06274C12.0223 6.536 11.6383 6.92017 11.165 6.92017H6.91797V11.1663C6.91797 11.6395 6.53374 12.0235 6.06055 12.0237C5.58716 12.0237 5.20312 11.6396 5.20312 11.1663V6.92017H0.957031C0.483929 6.91994 0.0997601 6.53586 0.0996094 6.06274C0.0996094 5.5895 0.483837 5.20555 0.957031 5.20532H5.20312V0.958252C5.20316 0.484897 5.58718 0.10083 6.06055 0.10083Z" fill="#7C4DFF"/>
83+
</g>
84+
<defs>
85+
<clipPath id="clip0_3001_2564">
86+
<rect width="12" height="12" fill="white"/>
87+
</clipPath>
88+
</defs>
89+
</svg>
90+
`

0 commit comments

Comments
 (0)