LaptopCardComponent.razor 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. @using RackPeek.Domain.Resources.Laptops
  2. @using RackPeek.Domain.Resources.SubResources
  3. @using RackPeek.Domain.UseCases.Cpus
  4. @using RackPeek.Domain.UseCases.Drives
  5. @using RackPeek.Domain.UseCases.Gpus
  6. @inject IGetResourceByNameUseCase<Laptop> GetByNameUseCase
  7. @inject UpdateLaptopUseCase UpdateUseCase
  8. @inject IDeleteResourceUseCase<Laptop> DeleteUseCase
  9. @inject IAddCpuUseCase<Laptop> AddCpuUseCase
  10. @inject IUpdateCpuUseCase<Laptop> UpdateCpuUseCase
  11. @inject IRemoveCpuUseCase<Laptop> RemoveCpuUseCase
  12. @inject IAddDriveUseCase<Laptop> AddDriveUseCase
  13. @inject IUpdateDriveUseCase<Laptop> UpdateDriveUseCase
  14. @inject IRemoveDriveUseCase<Laptop> RemoveDriveUseCase
  15. @inject IAddGpuUseCase<Laptop> AddGpuUseCase
  16. @inject IUpdateGpuUseCase<Laptop> UpdateGpuUseCase
  17. @inject IRemoveGpuUseCase<Laptop> RemoveGpuUseCase
  18. @inject ICloneResourceUseCase<Laptop> CloneUseCase
  19. @inject IRenameResourceUseCase<Laptop> RenameUseCase
  20. @inject NavigationManager Nav
  21. <div class="border border-zinc-800 rounded p-4 bg-zinc-900"
  22. data-testid=@($"laptop-item-{Laptop.Name.Replace(" ", "-")}")>
  23. <div class="flex justify-between items-center mb-3">
  24. <div class="text-zinc-100 hover:text-emerald-300">
  25. <NavLink href="@($"resources/hardware/{Uri.EscapeDataString(Laptop.Name)}")"
  26. class="block"
  27. data-testid=@($"laptop-item-{Laptop.Name.Replace(" ", "-")}-link")>
  28. @Laptop.Name
  29. </NavLink>
  30. </div>
  31. <div class="flex items-center gap-2">
  32. @if (!string.IsNullOrWhiteSpace(Laptop.Model))
  33. {
  34. <span class="text-xs text-zinc-400"
  35. data-testid="laptop-model-badge">
  36. @Laptop.Model
  37. </span>
  38. }
  39. <button data-testid="rename-laptop-button"
  40. class="text-xs text-blue-400 hover:text-blue-300 transition"
  41. @onclick="OpenRename">
  42. Rename
  43. </button>
  44. <button data-testid="clone-laptop-button"
  45. class="text-xs text-emerald-400 hover:text-emerald-300 transition"
  46. @onclick="OpenClone">
  47. Clone
  48. </button>
  49. <button data-testid="delete-laptop-button"
  50. class="text-xs text-red-400 hover:text-red-300 transition"
  51. @onclick="ConfirmDelete">
  52. Delete
  53. </button>
  54. </div>
  55. </div>
  56. <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
  57. <!-- CPU -->
  58. <div data-testid="laptop-cpu-section">
  59. <div class="flex items-center justify-between mb-1 group">
  60. <div class="text-zinc-400">
  61. CPU
  62. <button data-testid="add-cpu-button"
  63. class="hover:text-emerald-400 transition"
  64. @onclick="OpenAddCpu">
  65. +
  66. </button>
  67. </div>
  68. </div>
  69. @if (Laptop.Cpus?.Any() == true)
  70. {
  71. @foreach (var cpu in Laptop.Cpus)
  72. {
  73. <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  74. <button data-testid=@($"edit-cpu-{cpu.ToString().Replace(" ", "-")}")
  75. class="hover:text-emerald-400"
  76. @onclick="() => OpenEditCpu(cpu)">
  77. @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
  78. </button>
  79. </div>
  80. }
  81. }
  82. </div>
  83. <!-- RAM -->
  84. <div data-testid="laptop-ram-section">
  85. <div class="text-zinc-400 mb-1">
  86. RAM
  87. <button data-testid="edit-ram-button"
  88. class="hover:text-emerald-400 transition"
  89. @onclick="EditRam">
  90. +
  91. </button>
  92. </div>
  93. @if (Laptop.Ram is not null)
  94. {
  95. <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  96. <button data-testid="ram-value-button"
  97. class="hover:text-emerald-400"
  98. @onclick="EditRam">
  99. @($"{Laptop.Ram.Size} GB {Laptop.Ram.Mts} MT/s")
  100. </button>
  101. </div>
  102. }
  103. </div>
  104. <!-- Drives -->
  105. <div data-testid="laptop-drive-section">
  106. <div class="flex items-center justify-between mb-1 group">
  107. <div class="text-zinc-400">
  108. Drives
  109. <button data-testid="add-drive-button"
  110. class="hover:text-emerald-400 transition"
  111. @onclick="OpenAddDrive">
  112. +
  113. </button>
  114. </div>
  115. </div>
  116. @if (Laptop.Drives?.Any() == true)
  117. {
  118. @foreach (var drive in Laptop.Drives)
  119. {
  120. <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  121. <button data-testid=@($"edit-drive-{drive.Type}-{drive.Size}")
  122. class="hover:text-emerald-400"
  123. @onclick="() => OpenEditDrive(drive)">
  124. @drive.Type — @drive.Size GB
  125. </button>
  126. </div>
  127. }
  128. }
  129. </div>
  130. <!-- GPUs -->
  131. <div data-testid="laptop-gpu-section">
  132. <div class="flex items-center justify-between mb-1 group">
  133. <div class="text-zinc-400">
  134. GPUs
  135. <button data-testid="add-gpu-button"
  136. class="hover:text-emerald-400 transition"
  137. @onclick="OpenAddGpu">
  138. +
  139. </button>
  140. </div>
  141. </div>
  142. @if (Laptop.Gpus?.Any() == true)
  143. {
  144. @foreach (var gpu in Laptop.Gpus)
  145. {
  146. <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  147. <button data-testid=@($"edit-gpu-{gpu.Model}-{gpu.Vram}")
  148. class="hover:text-emerald-400"
  149. @onclick="() => OpenEditGpu(gpu)">
  150. @gpu.Model — @gpu.Vram GB VRAM
  151. </button>
  152. </div>
  153. }
  154. }
  155. </div>
  156. </div>
  157. <ResourceTagEditor Resource="Laptop"
  158. TestIdPrefix="laptop" />
  159. <ResourceLabelEditor Resource="Laptop"
  160. TestIdPrefix="laptop" />
  161. <div class="md:col-span-2"
  162. data-testid="laptop-notes-section">
  163. <div class="text-zinc-400 mb-1">Notes</div>
  164. @if (!_editingNotes)
  165. {
  166. <MarkdownViewer Value="@Laptop.Notes"
  167. ShowEditButton="true"
  168. OnEdit="BeginNotesEdit"
  169. TestIdPrefix="laptop-notes-viewer" />
  170. }
  171. else
  172. {
  173. <MarkdownEditor @bind-Value="_notesDraft"
  174. ShowActionButtons="true"
  175. OnSave="SaveNotes"
  176. OnCancel="CancelNotesEdit"
  177. TestIdPrefix="laptop-notes-editor" />
  178. }
  179. </div>
  180. </div>
  181. <CpuModal IsOpen="@_cpuModalOpen"
  182. IsOpenChanged="v => _cpuModalOpen = v"
  183. Value="@_editingCpu"
  184. OnSubmit="HandleCpuSubmit"
  185. OnDelete="HandleCpuDelete"
  186. TestIdPrefix="laptop"/>
  187. <RamModal IsOpen="@_isRamModalOpen"
  188. IsOpenChanged="v => _isRamModalOpen = v"
  189. Value="@Laptop.Ram"
  190. OnSubmit="HandleRamSubmit"
  191. TestIdPrefix="laptop"/>
  192. <DriveModal IsOpen="@_driveModalOpen"
  193. IsOpenChanged="v => _driveModalOpen = v"
  194. Value="@_editingDrive"
  195. OnSubmit="HandleDriveSubmit"
  196. OnDelete="HandleDriveDelete"
  197. TestIdPrefix="laptop"/>
  198. <GpuModal IsOpen="@_gpuModalOpen"
  199. IsOpenChanged="v => _gpuModalOpen = v"
  200. Value="@_editingGpu"
  201. OnSubmit="HandleGpuSubmit"
  202. OnDelete="HandleGpuDelete"
  203. TestIdPrefix="laptop"/>
  204. <ConfirmModal IsOpen="_confirmDeleteOpen"
  205. IsOpenChanged="v => _confirmDeleteOpen = v"
  206. Title="Delete laptop"
  207. ConfirmText="Delete"
  208. ConfirmClass="bg-red-600 hover:bg-red-500"
  209. OnConfirm="DeleteServer"
  210. TestIdPrefix="Laptop">
  211. Are you sure you want to delete <strong>@Laptop.Name</strong>?
  212. </ConfirmModal>
  213. <StringValueModal IsOpen="_renameOpen"
  214. IsOpenChanged="v => _renameOpen = v"
  215. Title="Rename laptop"
  216. Description="Enter a new name for this laptop"
  217. Label="New laptop name"
  218. Value="@Laptop.Name"
  219. OnSubmit="HandleRenameSubmit"
  220. TestIdPrefix="laptop-rename" />
  221. <StringValueModal IsOpen="_cloneOpen"
  222. IsOpenChanged="v => _cloneOpen = v"
  223. Title="Clone resource"
  224. Description="Enter a name for the cloned resource"
  225. Label="New resource name"
  226. Value="@($"{Laptop.Name}-copy")"
  227. OnSubmit="HandleCloneSubmit"
  228. TestIdPrefix="laptop-clone" />
  229. @code {
  230. [Parameter] [EditorRequired] public Laptop Laptop { get; set; } = default!;
  231. #region RAM
  232. private bool _isRamModalOpen;
  233. private void EditRam()
  234. {
  235. _isRamModalOpen = true;
  236. }
  237. private async Task HandleRamSubmit(Ram? value)
  238. {
  239. _isRamModalOpen = false;
  240. await UpdateUseCase.ExecuteAsync(Laptop.Name, Laptop.Model, value?.Size ?? 0, value?.Mts ?? 0, Laptop.Notes);
  241. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  242. }
  243. #endregion
  244. #region CPU
  245. bool _cpuModalOpen;
  246. int _editingCpuIndex;
  247. Cpu? _editingCpu;
  248. void OpenAddCpu()
  249. {
  250. _editingCpuIndex = -1;
  251. _editingCpu = null;
  252. _cpuModalOpen = true;
  253. }
  254. void OpenEditCpu(Cpu cpu)
  255. {
  256. _editingCpu = cpu;
  257. Laptop.Cpus ??= new List<Cpu>();
  258. _editingCpuIndex = Laptop.Cpus.IndexOf(cpu);
  259. ;
  260. _cpuModalOpen = true;
  261. }
  262. async Task HandleCpuSubmit(Cpu cpu)
  263. {
  264. Laptop.Cpus ??= new List<Cpu>();
  265. if (_editingCpuIndex < 0)
  266. {
  267. await AddCpuUseCase.ExecuteAsync(Laptop.Name, cpu.Model, cpu.Cores, cpu.Threads);
  268. }
  269. else
  270. {
  271. await UpdateCpuUseCase.ExecuteAsync(Laptop.Name, _editingCpuIndex, cpu.Model, cpu.Cores, cpu.Threads);
  272. }
  273. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  274. }
  275. async Task HandleCpuDelete(Cpu cpu)
  276. {
  277. await RemoveCpuUseCase.ExecuteAsync(Laptop.Name, _editingCpuIndex);
  278. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  279. }
  280. #endregion
  281. #region Drives
  282. bool _driveModalOpen;
  283. int _editingDriveIndex;
  284. Drive? _editingDrive;
  285. void OpenAddDrive()
  286. {
  287. _editingDriveIndex = -1;
  288. _editingDrive = null;
  289. _driveModalOpen = true;
  290. }
  291. void OpenEditDrive(Drive drive)
  292. {
  293. _editingDrive = drive;
  294. Laptop.Drives ??= new List<Drive>();
  295. _editingDriveIndex = Laptop.Drives.IndexOf(drive);
  296. ;
  297. _driveModalOpen = true;
  298. }
  299. async Task HandleDriveSubmit(Drive drive)
  300. {
  301. Laptop.Drives ??= new List<Drive>();
  302. if (_editingDriveIndex < 0)
  303. {
  304. await AddDriveUseCase.ExecuteAsync(Laptop.Name, drive.Type, drive.Size);
  305. }
  306. else
  307. {
  308. await UpdateDriveUseCase.ExecuteAsync(Laptop.Name, _editingDriveIndex, drive.Type, drive.Size);
  309. }
  310. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  311. StateHasChanged();
  312. }
  313. async Task HandleDriveDelete(Drive drive)
  314. {
  315. await RemoveDriveUseCase.ExecuteAsync(Laptop.Name, _editingDriveIndex);
  316. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  317. StateHasChanged();
  318. }
  319. #endregion
  320. #region GPUs
  321. bool _gpuModalOpen;
  322. int _editingGpuIndex;
  323. Gpu? _editingGpu;
  324. void OpenAddGpu()
  325. {
  326. _editingGpuIndex = -1;
  327. _editingGpu = null;
  328. _gpuModalOpen = true;
  329. }
  330. void OpenEditGpu(Gpu gpu)
  331. {
  332. Laptop.Gpus ??= new List<Gpu>();
  333. _editingGpuIndex = Laptop.Gpus.IndexOf(gpu);
  334. _editingGpu = gpu;
  335. _gpuModalOpen = true;
  336. }
  337. async Task HandleGpuSubmit(Gpu gpu)
  338. {
  339. Laptop.Gpus ??= new List<Gpu>();
  340. if (_editingGpuIndex < 0)
  341. {
  342. await AddGpuUseCase.ExecuteAsync(
  343. Laptop.Name,
  344. gpu.Model,
  345. gpu.Vram);
  346. }
  347. else
  348. {
  349. await UpdateGpuUseCase.ExecuteAsync(
  350. Laptop.Name,
  351. _editingGpuIndex,
  352. gpu.Model,
  353. gpu.Vram);
  354. }
  355. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  356. }
  357. async Task HandleGpuDelete(Gpu gpu)
  358. {
  359. await RemoveGpuUseCase.ExecuteAsync(Laptop.Name, _editingGpuIndex);
  360. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  361. }
  362. #endregion
  363. }
  364. @code {
  365. private bool _confirmDeleteOpen;
  366. [Parameter] public EventCallback<string> OnDeleted { get; set; }
  367. void ConfirmDelete()
  368. {
  369. _confirmDeleteOpen = true;
  370. }
  371. async Task DeleteServer()
  372. {
  373. _confirmDeleteOpen = false;
  374. await DeleteUseCase.ExecuteAsync(Laptop.Name);
  375. if (OnDeleted.HasDelegate)
  376. await OnDeleted.InvokeAsync(Laptop.Name);
  377. }
  378. }
  379. @code
  380. {
  381. bool _renameOpen;
  382. void OpenRename()
  383. {
  384. _renameOpen = true;
  385. }
  386. async Task HandleRenameSubmit(string newName)
  387. {
  388. await RenameUseCase.ExecuteAsync(Laptop.Name, newName);
  389. Nav.NavigateTo($"resources/hardware/{Uri.EscapeDataString(newName)}");
  390. }
  391. }
  392. @code
  393. {
  394. bool _cloneOpen;
  395. void OpenClone()
  396. {
  397. _cloneOpen = true;
  398. }
  399. async Task HandleCloneSubmit(string newName)
  400. {
  401. await CloneUseCase.ExecuteAsync(Laptop.Name, newName);
  402. Nav.NavigateTo($"resources/hardware/{Uri.EscapeDataString(newName)}");
  403. }
  404. }
  405. @code
  406. {
  407. bool _editingNotes;
  408. string? _notesDraft;
  409. void BeginNotesEdit()
  410. {
  411. _editingNotes = true;
  412. _notesDraft = Laptop.Notes; // draft buffer
  413. }
  414. void CancelNotesEdit()
  415. {
  416. _editingNotes = false;
  417. _notesDraft = null; // discard
  418. }
  419. async Task SaveNotes()
  420. {
  421. _editingNotes = false;
  422. await UpdateUseCase.ExecuteAsync(
  423. Laptop.Name,
  424. Laptop.Model,
  425. Laptop.Ram?.Size,
  426. Laptop.Ram?.Mts,
  427. _notesDraft);
  428. Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
  429. _notesDraft = null;
  430. }
  431. }