refact(frontend): restore glassmorphism card style and adjust layout padding
Hee Sung Son committed
Apr 5, 2026 at 22:55 UTC
6a782ea054e1f179d32314a118b4122a50b9f32b
3 files changed
+143
-136
frontend/src/components/SearchBar.tsx
+1
-1
@@ -36,7 +36,7 @@ export function SearchBar({
36
setShowFilterModal,
37
}: SearchBarProps) {
38
return (
39
- <div className="flex flex-wrap mt-4 sm:mt-6 items-center gap-3 px-4 sm:px-6">
39
+ <div className="flex flex-wrap mt-4 sm:mt-6 items-center gap-3 px-0">
40
<div className="flex gap-2 items-center w-full">
41
<label className="flex min-w-55 flex-1 items-stretch h-10 relative">
42
<div className="left-0 h-full absolute text-text-muted flex items-center justify-center pl-3 pr-2 rounded-l-md bg-border border border-border">
frontend/src/components/ServerCard.tsx
+138
-131
@@ -59,14 +59,14 @@ export function ServerCard({
59
owner,
60
online,
61
firstSeen,
62
- dns,
62
+ dns: _dns,
63
navigationPath,
64
navigationState,
65
isFavorite = false,
66
onToggleFavorite,
67
showAdminControls = false,
68
identityKey,
69
- address,
69
+ address: _address,
70
isBanned = false,
71
isApproved = false,
72
isDenied = false,
@@ -192,7 +192,7 @@ export function ServerCard({
192
};
193
194
const formatStepLabel = (value: number): string => {
195
- if (value === 0) return "unl";
195
+ if (value === 0) return "∞";
196
if (value >= 1000000) return `${value / 1000000}M`;
197
if (value >= 1000) return `${value / 1000}K`;
198
return value.toString();
@@ -228,55 +228,58 @@ export function ServerCard({
228
<article
229
data-hero-key={`server-bg-${serverId}`}
230
className={clsx(
231
- "group flex h-full flex-col overflow-hidden rounded-xl border border-border bg-card shadow-[0_18px_42px_oklch(0%_0_0/0.06)] transition-transform duration-200 hover:-translate-y-1 hover:shadow-[0_24px_56px_oklch(0%_0_0/0.08)]",
232
- showAdminControls ? "min-h-90" : "min-h-77.5"
231
+ "relative w-full overflow-hidden rounded-3xl group border border-white/10 shadow-lg",
232
+ showAdminControls ? "h-71.5" : "h-[174.5px]"
233
)}
234
>
235
<div
236
- className="relative h-40 overflow-hidden border-b border-border bg-secondary"
236
+ className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105"
237
style={{
238
backgroundImage: thumbnail
239
- ? `linear-gradient(rgba(255,255,255,0.08), rgba(255,255,255,0.08)), url(${thumbnail})`
240
- : "linear-gradient(135deg, var(--card-media-fallback-start) 0%, var(--card-media-fallback-end) 100%)",
241
- backgroundSize: "cover",
242
- backgroundPosition: "center",
239
+ ? `url(${thumbnail})`
240
+ : "linear-gradient(135deg, var(--card) 0%, var(--background) 100%)",
241
}}
244
- >
245
- {!thumbnail && (
246
- <div
247
- className="absolute inset-0"
248
- style={{ background: "var(--card-media-overlay)" }}
249
- />
250
- )}
242
+ />
243
252
- <div className="absolute inset-x-0 top-0 flex items-start justify-between p-4">
253
- <div className="inline-flex items-center gap-2 rounded-full border border-border bg-card/95 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground shadow-[0_8px_18px_oklch(0%_0_0/0.05)]">
254
- <span
244
+ <div className="absolute inset-0 bg-linear-to-t from-black via-black/60 to-transparent" />
245
+
246
+ <div className="relative z-10 flex h-full flex-col justify-between p-5">
247
+ <div className="flex items-start justify-between">
248
+ <div className="flex items-center gap-2 rounded-full bg-black/40 px-3 py-1 backdrop-blur-sm border border-white/5">
249
+ <div
250
className={clsx(
256
- "h-2.5 w-2.5 rounded-full",
257
- online ? "bg-green-status" : "bg-muted-foreground"
251
+ "size-2 rounded-full",
252
+ online
253
+ ? "bg-primary shadow-[0_0_8px_rgba(0,219,219,0.8)] animate-pulse"
254
+ : "bg-gray-500"
255
)}
256
/>
260
- <span>
257
+ <span
258
+ className={clsx(
259
+ "text-[10px] font-bold uppercase tracking-wider",
260
+ online ? "text-white" : "text-white/60"
261
+ )}
262
+ >
263
{online ? "Online" : "Offline"}
264
{formattedDuration && online && ` · ${formattedDuration}`}
265
</span>
266
</div>
267
+
268
{showAdminControls ? (
269
<button
270
onClick={handleSelectClick}
271
className={clsx(
269
- "flex size-9 items-center justify-center rounded-full border border-border bg-card/95 text-text-muted shadow-[0_8px_18px_oklch(0%_0_0/0.05)] transition-colors cursor-pointer",
272
+ "flex size-8 items-center justify-center rounded-full backdrop-blur-md transition-colors border border-white/5 cursor-pointer",
273
isSelected
271
- ? "bg-primary text-primary-foreground"
272
- : "hover:text-foreground"
274
+ ? "bg-primary text-black"
275
+ : "bg-black/40 text-white/70 hover:bg-primary hover:text-black"
276
)}
277
aria-label={isSelected ? "Deselect" : "Select"}
278
>
279
<svg
280
xmlns="http://www.w3.org/2000/svg"
281
viewBox="0 0 24 24"
279
- className="h-4.5 w-4.5"
282
+ className="w-4.5 h-4.5"
283
fill="none"
284
stroke="currentColor"
285
strokeWidth="3"
@@ -290,10 +293,10 @@ export function ServerCard({
293
<button
294
onClick={handleFavoriteClick}
295
className={clsx(
293
- "flex size-9 items-center justify-center rounded-full border border-border bg-card/95 text-text-muted shadow-[0_8px_18px_oklch(0%_0_0/0.05)] transition-colors cursor-pointer",
296
+ "flex size-8 items-center justify-center rounded-full backdrop-blur-md transition-colors border border-white/5 cursor-pointer",
297
isFavorite
295
- ? "bg-primary text-primary-foreground"
296
- : "hover:text-foreground"
298
+ ? "bg-primary text-black"
299
+ : "bg-black/40 text-white/70 hover:bg-primary hover:text-black"
300
)}
301
aria-label={
302
isFavorite ? "Remove from favorites" : "Add to favorites"
@@ -302,7 +305,7 @@ export function ServerCard({
305
<svg
306
xmlns="http://www.w3.org/2000/svg"
307
viewBox="0 0 24 24"
305
- className="h-4.5 w-4.5"
308
+ className="w-4.5 h-4.5"
309
fill={isFavorite ? "currentColor" : "none"}
310
stroke="currentColor"
311
strokeWidth="2"
@@ -314,113 +317,117 @@ export function ServerCard({
317
</button>
318
)}
319
</div>
317
- </div>
318
-
319
- <div className="flex flex-1 flex-col justify-between gap-4 p-5">
320
- <div className="space-y-4">
321
- <div className="space-y-2">
322
- <h3 className="truncate text-2xl font-bold tracking-tight text-foreground">
323
- {name}
324
- </h3>
325
- {description && (
326
- <p className="line-clamp-2 text-sm leading-6 text-text-muted">
327
- {description}
328
- </p>
329
- )}
330
- </div>
320
332
- {tags.length > 0 && (
333
- <div className="w-full overflow-x-auto">
334
- <div className="flex min-w-max gap-2 pb-1">
335
- {tags.map((tag, index) => (
336
- <span
337
- key={index}
338
- className="whitespace-nowrap rounded-full border border-border bg-secondary px-3 py-1 text-xs font-medium text-text-muted"
339
- >
340
- #{tag}
341
- </span>
342
- ))}
343
- </div>
344
- </div>
345
- )}
321
+ <div className="flex flex-col gap-3">
322
+ <div className="flex items-end justify-between gap-3">
323
+ <div className="flex flex-col gap-1.5 flex-1 min-w-0">
324
+ <h3 className="font-display text-xl font-bold leading-tight text-white truncate">
325
+ {name}
326
+ </h3>
327
+
328
+ {description && (
329
+ <p className="text-xs text-white/70 line-clamp-1 font-medium">
330
+ {description}
331
+ </p>
332
+ )}
333
347
- <div className="flex flex-wrap items-center gap-3 text-sm text-text-muted">
348
- {owner && <span>by {owner}</span>}
349
- {dns && (
350
- <span className="rounded-full bg-secondary px-3 py-1 font-mono text-[11px] text-secondary-foreground">
351
- {dns}
352
- </span>
353
- )}
354
- </div>
355
- </div>
334
+ {tags && tags.length > 0 && (
335
+ <div className="w-full overflow-x-auto mt-1">
336
+ <div className="flex gap-1.5 min-w-max">
337
+ {tags.map((tag, index) => (
338
+ <span
339
+ key={index}
340
+ className="rounded bg-primary/20 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-primary border border-primary/30 whitespace-nowrap"
341
+ >
342
+ #{tag}
343
+ </span>
344
+ ))}
345
+ </div>
346
+ </div>
347
+ )}
348
357
- {showAdminControls && address && (
358
- <div className="flex flex-col gap-3 rounded-3xl border border-border bg-secondary/70 p-4">
359
- <div className="text-[11px] text-text-muted">
360
- Address: <span className="font-mono text-foreground">{address}</span>
349
+ {owner && (
350
+ <span className="text-[10px] font-medium text-white/50">
351
+ by {owner}
352
+ </span>
353
+ )}
354
</div>
355
363
- {onBPSChange && (
364
- <div className="flex items-center justify-between gap-4">
365
- <span className="text-xs text-text-muted">
366
- BPS: <span className="font-medium text-foreground">{formatBPS(bps)}</span>
367
- </span>
368
- <button
369
- onClick={handleBPSSettingsClick}
370
- className="rounded-full border border-border bg-card px-3 py-1 text-[11px] font-semibold text-foreground transition-colors hover:bg-secondary cursor-pointer"
371
- >
372
- Settings
373
- </button>
356
+ {!showAdminControls && thumbnail && (
357
+ <div className="shrink-0">
358
+ <div className="size-10 overflow-hidden rounded-xl border border-white/20 shadow-lg">
359
+ <img
360
+ alt={`${name} avatar`}
361
+ className="h-full w-full object-cover"
362
+ src={thumbnail}
363
+ />
364
+ </div>
365
</div>
366
)}
367
+ </div>
368
377
- {isApproved && ip && (
378
- <div className="text-[11px] text-text-muted">
379
- IP: <span className="font-mono text-foreground">{displayIP || ip}</span>
380
- {isIPBanned && (
381
- <span className="ml-2 font-medium text-destructive">
382
- (Banned)
369
+ {showAdminControls && identityKey && (
370
+ <div className="flex flex-col gap-2 w-full mt-2">
371
+ {onBPSChange && (
372
+ <div className="flex items-center justify-between w-full">
373
+ <span className="text-xs text-white/60">
374
+ BPS: <span className="font-medium text-white">{formatBPS(bps)}</span>
375
</span>
384
- )}
385
- </div>
386
- )}
376
+ <button
377
+ onClick={handleBPSSettingsClick}
378
+ className="px-3 py-1 text-[10px] rounded-full bg-white/10 hover:bg-white/20 text-white/80 transition-colors cursor-pointer border border-white/10"
379
+ >
380
+ Settings
381
+ </button>
382
+ </div>
383
+ )}
384
388
- {!isApproved && !isDenied ? (
389
- <div className="flex gap-2">
390
- <button
391
- onClick={handleApproveClick}
392
- className="flex-1 rounded-xl bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground transition-opacity hover:opacity-90 cursor-pointer"
393
- >
394
- Approve
395
- </button>
385
+ {isApproved && ip && (
386
+ <div className="text-[10px] text-white/50">
387
+ IP: <span className="font-mono">{displayIP || ip}</span>
388
+ {isIPBanned && (
389
+ <span className="ml-2 text-red-400">(Banned)</span>
390
+ )}
391
+ </div>
392
+ )}
393
+
394
+ {!isApproved && !isDenied ? (
395
+ <div className="flex gap-2 w-full">
396
+ <button
397
+ onClick={handleApproveClick}
398
+ className="flex-1 px-4 py-2 rounded-lg font-medium text-xs transition-colors cursor-pointer text-white bg-green-600/80 hover:bg-green-600 backdrop-blur-sm"
399
+ >
400
+ Approve
401
+ </button>
402
+ <button
403
+ onClick={handleDenyClick}
404
+ className="flex-1 px-4 py-2 rounded-lg font-medium text-xs transition-colors cursor-pointer text-white bg-red-600/80 hover:bg-red-600 backdrop-blur-sm"
405
+ >
406
+ Deny
407
+ </button>
408
+ </div>
409
+ ) : (
410
<button
397
- onClick={handleDenyClick}
398
- className="flex-1 rounded-xl bg-destructive px-4 py-2 text-xs font-semibold text-destructive-foreground transition-opacity hover:opacity-90 cursor-pointer"
411
+ onClick={ip ? handleIPBanClick : handleBanClick}
412
+ className={clsx(
413
+ "w-full px-4 py-2 rounded-lg font-medium text-xs transition-colors cursor-pointer text-white backdrop-blur-sm",
414
+ (ip ? isIPBanned : isBanned)
415
+ ? "bg-green-600/80 hover:bg-green-600"
416
+ : "bg-red-600/80 hover:bg-red-600"
417
+ )}
418
>
400
- Deny
419
+ {ip
420
+ ? isIPBanned
421
+ ? "Unban IP"
422
+ : "Ban IP"
423
+ : isBanned
424
+ ? "Unban"
425
+ : "Ban"}
426
</button>
402
- </div>
403
- ) : (
404
- <button
405
- onClick={ip ? handleIPBanClick : handleBanClick}
406
- className={clsx(
407
- "w-full rounded-xl px-4 py-2 text-xs font-semibold transition-opacity hover:opacity-90 cursor-pointer",
408
- (ip ? isIPBanned : isBanned)
409
- ? "bg-green-status text-white"
410
- : "bg-destructive text-destructive-foreground"
411
- )}
412
- >
413
- {ip
414
- ? isIPBanned
415
- ? "Unban IP"
416
- : "Ban IP"
417
- : isBanned
418
- ? "Unban"
419
- : "Ban"}
420
- </button>
421
- )}
422
- </div>
423
- )}
427
+ )}
428
+ </div>
429
+ )}
430
+ </div>
431
</div>
432
</article>
433
);
@@ -433,7 +440,7 @@ export function ServerCard({
440
<Link
441
to={navigationPath}
442
state={navigationState}
436
- className="relative block h-full cursor-pointer"
443
+ className="relative cursor-pointer block"
444
>
445
{cardBody}
446
</Link>
@@ -459,15 +466,15 @@ export function ServerCard({
466
const idx = parseInt(event.target.value, 10);
467
handleSliderChange(idx);
468
}}
462
- className="h-2 w-full cursor-pointer appearance-none rounded-md bg-secondary"
469
+ className="w-full h-2 bg-secondary rounded-md appearance-none cursor-pointer"
470
/>
471
<div className="flex justify-between text-xs text-text-muted">
472
{bpsSteps.map((step, idx) => (
473
<span
474
key={idx}
475
className={clsx(
469
- "cursor-pointer transition-colors hover:text-foreground",
470
- sliderIndex === idx && "font-medium text-primary"
476
+ "cursor-pointer hover:text-foreground transition-colors",
477
+ sliderIndex === idx && "text-primary font-medium"
478
)}
479
onClick={() => handleSliderChange(idx)}
480
>
@@ -476,7 +483,7 @@ export function ServerCard({
483
))}
484
</div>
485
<div>
479
- <label className="mb-1 block text-xs text-text-muted">
486
+ <label className="text-xs text-text-muted mb-1 block">
487
Custom value (B/s)
488
</label>
489
<input
@@ -486,7 +493,7 @@ export function ServerCard({
493
setBpsInput(event.target.value);
494
syncSliderFromInput(parseInt(event.target.value, 10) || 0);
495
}}
489
- className="w-full rounded border border-foreground/20 bg-background px-3 py-2 text-foreground"
496
+ className="w-full px-3 py-2 border border-foreground/20 rounded bg-background text-foreground"
497
placeholder="Enter BPS limit"
498
min="0"
499
/>
frontend/src/components/ServerListView.tsx
+4
-4
@@ -668,7 +668,7 @@ export function ServerListView({
668
};
669
670
const gridClasses =
671
- "grid grid-cols-1 gap-6 p-4 min-[500px]:grid-cols-2 min-[500px]:p-6 md:grid-cols-3";
671
+ `grid grid-cols-1 gap-6 ${isAdmin ? "p-4 min-[500px]:p-6" : "py-4 min-[500px]:py-6"} min-[500px]:grid-cols-2 md:grid-cols-3`;
672
const serverCards = serverRows.map(renderServerCard);
673
const serverGrid =
674
serverCards.length > 0 ? (
@@ -740,11 +740,11 @@ export function ServerListView({
740
<div className="flex items-center gap-2">
741
<div className="flex-1">{searchBar}</div>
742
</div>
743
- <div className="mt-4 hidden flex-wrap items-center gap-6 px-4 sm:flex sm:px-6">
743
+ <div className="mt-4 hidden flex-wrap items-center gap-6 sm:flex">
744
{adminFilterControls}
745
</div>
746
{onApprovalModeChange && (
747
- <div className="mt-4 flex items-center gap-3 px-4 sm:hidden">
747
+ <div className="mt-4 flex items-center gap-3 sm:hidden">
748
<span className="text-sm font-medium text-text-muted">
749
Approval
750
</span>
@@ -785,7 +785,7 @@ export function ServerListView({
785
</div>
786
)}
787
</div>
788
- <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-0 md:px-8">
788
+ <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-0">
789
<main className="z-0 flex-1">
790
{serverGrid ?? (
791
<div className="py-12 text-center">