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">