Skip to content

Commit c8b5acf

Browse files
authored
Merge pull request #2858 from Brain-up/refactor/extract-headphones-component
refactor: extract headphones UI into dedicated component
2 parents 328009b + 1abbf49 commit c8b5acf

5 files changed

Lines changed: 457 additions & 213 deletions

File tree

frontend/app/components/profile/index.gts

Lines changed: 5 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,22 @@ import NetworkService, { UserDTO } from 'brn/services/network';
1212
import { isBornYearValid, isNotEmptyString } from 'brn/utils/validators';
1313
import { LinkTo } from '@ember/routing';
1414
import { on } from '@ember/modifier';
15-
import { fn } from '@ember/helper';
16-
import { concat } from '@ember/helper';
15+
import { fn, concat } from '@ember/helper';
1716
import { t } from 'ember-intl';
1817
import { eq } from 'ember-truth-helpers';
1918
import htmlSafe from 'brn/helpers/html-safe';
2019
import ModalDialog from 'ember-modal-dialog/components/modal-dialog';
2120
import UiAvatars from 'brn/components/ui/avatars';
2221
import LoginFormInput from 'brn/components/login-form/input';
23-
import UiConfirmDialog from 'brn/components/ui/confirm-dialog';
24-
import type Store from 'brn/services/store';
25-
import type { Headphone } from 'brn/schemas/headphone';
26-
import didInsert from '@ember/render-modifiers/modifiers/did-insert';
27-
28-
const HEADPHONE_TYPES = [
29-
{ value: 'NOT_DEFINED', label: 'Not defined' },
30-
{ value: 'ON_EAR_BLUETOOTH', label: 'On-ear Bluetooth' },
31-
{ value: 'OVER_EAR_BLUETOOTH', label: 'Over-ear Bluetooth' },
32-
{ value: 'IN_EAR_BLUETOOTH', label: 'In-ear Bluetooth' },
33-
{ value: 'ON_EAR_NO_BLUETOOTH', label: 'On-ear Wired' },
34-
{ value: 'OVER_EAR_NO_BLUETOOTH', label: 'Over-ear Wired' },
35-
{ value: 'IN_EAR_NO_BLUETOOTH', label: 'In-ear Wired' },
36-
] as const;
37-
38-
function headphoneTypeLabel(type: string): string {
39-
const found = HEADPHONE_TYPES.find((t) => t.value === type);
40-
return found ? found.label : type;
41-
}
22+
import UiHeadphones from 'brn/components/ui/headphones';
4223

4324
export default class ProfileComponent extends Component {
4425
@service('intl') intl!: IntlService;
4526
@service('session') session!: Session;
4627
@service('user-data') userData!: UserDataService;
4728
@service('network') network!: NetworkService;
48-
@service('store') store!: Store;
4929

5030
@tracked showAvatarsModal = false;
51-
@tracked showAddHeadphonesForm = false;
52-
@tracked headphones: Headphone[] = [];
53-
@tracked headphoneName = '';
54-
@tracked headphoneType = 'NOT_DEFINED';
55-
@tracked headphoneError = '';
56-
@tracked isLoadingHeadphones = false;
57-
@tracked headphonePendingDelete: Headphone | null = null;
5831

5932
get avatarUrl() {
6033
return this.userData.avatarUrl;
@@ -178,83 +151,6 @@ export default class ProfileComponent extends Component {
178151
}
179152
}
180153

181-
@action
182-
async loadHeadphones() {
183-
this.isLoadingHeadphones = true;
184-
try {
185-
this.headphones = await this.store.findAll<Headphone>('headphone');
186-
} catch (error) {
187-
console.error('Failed to load headphones:', error);
188-
}
189-
this.isLoadingHeadphones = false;
190-
}
191-
192-
@action
193-
toggleAddHeadphonesForm() {
194-
this.showAddHeadphonesForm = !this.showAddHeadphonesForm;
195-
this.headphoneName = '';
196-
this.headphoneType = 'NOT_DEFINED';
197-
this.headphoneError = '';
198-
}
199-
200-
@action
201-
onHeadphoneNameInput(e: Event & { target: HTMLInputElement }) {
202-
this.headphoneName = e.target.value;
203-
this.headphoneError = '';
204-
}
205-
206-
@action
207-
onHeadphoneTypeChange(e: Event & { target: HTMLSelectElement }) {
208-
this.headphoneType = e.target.value;
209-
}
210-
211-
@action
212-
async addHeadphones(e: Event) {
213-
e.preventDefault();
214-
const name = this.headphoneName.trim();
215-
if (!name) {
216-
this.headphoneError = this.intl.t('profile.headphones.name_required');
217-
return;
218-
}
219-
try {
220-
await this.network.addHeadphones({
221-
name,
222-
type: this.headphoneType,
223-
active: true,
224-
});
225-
this.showAddHeadphonesForm = false;
226-
this.headphoneName = '';
227-
this.headphoneType = 'NOT_DEFINED';
228-
this.headphoneError = '';
229-
await this.loadHeadphones();
230-
} catch (error: any) {
231-
this.headphoneError = error.message || 'Failed to add headphones';
232-
}
233-
}
234-
235-
@action
236-
requestDeleteHeadphones(headphone: Headphone) {
237-
this.headphonePendingDelete = headphone;
238-
}
239-
240-
@action
241-
cancelDeleteHeadphones() {
242-
this.headphonePendingDelete = null;
243-
}
244-
245-
@action
246-
async confirmDeleteHeadphones() {
247-
const headphone = this.headphonePendingDelete;
248-
if (!headphone) return;
249-
this.headphonePendingDelete = null;
250-
try {
251-
await this.network.deleteHeadphones(String(headphone.id));
252-
await this.loadHeadphones();
253-
} catch (error) {
254-
console.error('Failed to delete headphones:', error);
255-
}
256-
}
257-
258154
<template>
259155
{{#if this.showAvatarsModal}}
260156
<ModalDialog
@@ -281,7 +177,7 @@ export default class ProfileComponent extends Component {
281177
>
282178
</button>
283179
</div>
284-
<div class="sm:p-8 lg:p-12 p-4" {{didInsert this.loadHeadphones}}>
180+
<div class="sm:p-8 lg:p-12 p-4">
285181
<div class="mb-4">
286182
<LoginFormInput
287183
@model={{this.user}}
@@ -345,104 +241,8 @@ export default class ProfileComponent extends Component {
345241
</label>
346242
</div>
347243

348-
<div class="mb-4" role="region" aria-label={{t "profile.headphones.title"}}>
349-
<p class="mb-2 text-sm font-bold text-gray-700">
350-
{{t "profile.headphones.title"}}
351-
</p>
352-
353-
{{#if this.isLoadingHeadphones}}
354-
<div class="animate-pulse space-y-2">
355-
<div class="h-16 bg-gray-200 rounded"></div>
356-
</div>
357-
{{else}}
358-
{{#each this.headphones as |headphone|}}
359-
<div data-test-headphone-item class="flex items-center justify-between p-3 mb-2 bg-gray-50 border border-gray-200 rounded-lg">
360-
<div>
361-
<p class="text-sm font-medium text-gray-800" data-test-headphone-name>{{headphone.name}}</p>
362-
<p class="text-xs text-gray-500" data-test-headphone-type>{{headphoneTypeLabel headphone.type}}</p>
363-
</div>
364-
<button
365-
data-test-delete-headphone
366-
type="button"
367-
aria-label={{t "profile.headphones.delete"}}
368-
class="btn-press hover:text-red-700 hover:bg-red-100 min-w-[44px] min-h-[44px] p-2 text-red-500 rounded-full flex items-center justify-center"
369-
title={{t "profile.headphones.delete"}}
370-
{{on "click" (fn this.requestDeleteHeadphones headphone)}}
371-
>
372-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
373-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
374-
</svg>
375-
</button>
376-
</div>
377-
{{/each}}
378-
379-
{{#if (eq this.headphones.length 0)}}
380-
<p class="text-sm text-gray-400 mb-2">{{t "profile.headphones.empty"}}</p>
381-
{{/if}}
382-
383-
{{#if this.showAddHeadphonesForm}}
384-
<form data-test-add-headphones-form class="p-3 mt-2 bg-gray-50 border border-gray-200 rounded-lg" {{on "submit" this.addHeadphones}}>
385-
<div class="mb-2">
386-
<label class="block mb-1 text-xs font-medium text-gray-600" for="headphone-name">
387-
{{t "profile.headphones.name_label"}}
388-
</label>
389-
<input
390-
data-test-headphone-name-input
391-
id="headphone-name"
392-
type="text"
393-
value={{this.headphoneName}}
394-
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full px-3 py-2 text-sm border border-gray-300 rounded-md"
395-
placeholder={{t "profile.headphones.name_placeholder"}}
396-
{{on "input" this.onHeadphoneNameInput}}
397-
/>
398-
</div>
399-
<div class="mb-2">
400-
<label class="block mb-1 text-xs font-medium text-gray-600" for="headphone-type">
401-
{{t "profile.headphones.type_label"}}
402-
</label>
403-
<select
404-
data-test-headphone-type-select
405-
id="headphone-type"
406-
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full px-3 py-2 text-sm border border-gray-300 rounded-md"
407-
{{on "change" this.onHeadphoneTypeChange}}
408-
>
409-
{{#each HEADPHONE_TYPES as |hType|}}
410-
<option value={{hType.value}} selected={{eq this.headphoneType hType.value}}>{{hType.label}}</option>
411-
{{/each}}
412-
</select>
413-
</div>
414-
{{#if this.headphoneError}}
415-
<p data-test-headphone-error class="mb-2 text-xs text-red-500">{{this.headphoneError}}</p>
416-
{{/if}}
417-
<div class="flex gap-2">
418-
<button
419-
data-test-submit-headphone
420-
type="submit"
421-
class="btn-press hover:bg-indigo-700 px-4 py-2 text-xs font-medium text-white bg-indigo-600 rounded-md"
422-
>
423-
{{t "profile.headphones.add_button"}}
424-
</button>
425-
<button
426-
data-test-cancel-headphone
427-
type="button"
428-
class="btn-press hover:bg-gray-200 px-4 py-2 text-xs font-medium text-gray-700 bg-gray-100 rounded-md"
429-
{{on "click" this.toggleAddHeadphonesForm}}
430-
>
431-
{{t "profile.headphones.cancel"}}
432-
</button>
433-
</div>
434-
</form>
435-
{{else}}
436-
<button
437-
data-test-show-add-headphones
438-
type="button"
439-
class="btn-press hover:text-indigo-700 mt-1 text-sm font-medium text-indigo-500"
440-
{{on "click" this.toggleAddHeadphonesForm}}
441-
>
442-
+ {{t "profile.headphones.add"}}
443-
</button>
444-
{{/if}}
445-
{{/if}}
244+
<div class="mb-4">
245+
<UiHeadphones />
446246
</div>
447247

448248
<div class="mb-4">
@@ -462,13 +262,5 @@ export default class ProfileComponent extends Component {
462262
</div>
463263
</div>
464264
</section>
465-
{{#if this.headphonePendingDelete}}
466-
<UiConfirmDialog
467-
@message={{t "profile.headphones.confirm_delete"}}
468-
@onConfirm={{this.confirmDeleteHeadphones}}
469-
@onCancel={{this.cancelDeleteHeadphones}}
470-
@destructive={{true}}
471-
/>
472-
{{/if}}
473265
</template>
474266
}

0 commit comments

Comments
 (0)