@@ -12,49 +12,22 @@ import NetworkService, { UserDTO } from 'brn/services/network';
1212import { isBornYearValid , isNotEmptyString } from ' brn/utils/validators' ;
1313import { LinkTo } from ' @ember/routing' ;
1414import { on } from ' @ember/modifier' ;
15- import { fn } from ' @ember/helper' ;
16- import { concat } from ' @ember/helper' ;
15+ import { fn , concat } from ' @ember/helper' ;
1716import { t } from ' ember-intl' ;
1817import { eq } from ' ember-truth-helpers' ;
1918import htmlSafe from ' brn/helpers/html-safe' ;
2019import ModalDialog from ' ember-modal-dialog/components/modal-dialog' ;
2120import UiAvatars from ' brn/components/ui/avatars' ;
2221import 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
4324export 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