mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-06-03 15:02:56 +00:00
ci: update docker build workflow and refine frontend theme
Refactor the CI/CD pipeline to use Docker Buildx for more efficient builds and implement automated image tagging and pushing to GHCR. On the frontend, update the theme system to use a neutral zinc-based dark mode instead of the previous warm dark theme. This includes: - Updating CSS variables in `globals.css` for a more consistent neutral palette. - Replacing `ring` color usage with `muted-foreground` in various UI components to align with the new design language. - Adjusting component backgrounds (e.g., `Header`, `Input`, `WidgetCard`) to use `bg-card` for better visual layering. - Simplifying component styles and removing unnecessary gradients.
This commit is contained in:
@@ -3,22 +3,85 @@ name: Docker Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
tags: ["v*"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_PREFIX: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-backend:
|
build-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build backend image
|
- name: Set up Docker Buildx
|
||||||
run: docker build -f backend/Dockerfile .
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry (push only)
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/backend
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=sha
|
||||||
|
|
||||||
|
- name: Build and push backend image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: backend/Dockerfile
|
||||||
|
push: ${{ github.event_name == 'push' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build frontend image
|
- name: Set up Docker Buildx
|
||||||
run: docker build -f frontend/Dockerfile ./frontend
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry (push only)
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/frontend
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=sha
|
||||||
|
|
||||||
|
- name: Build and push frontend image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./frontend
|
||||||
|
file: frontend/Dockerfile
|
||||||
|
push: ${{ github.event_name == 'push' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
+28
-28
@@ -31,28 +31,28 @@
|
|||||||
--font-geist-mono: "Geist Mono", "ui-monospace", "SFMono-Regular", "Roboto Mono", monospace;
|
--font-geist-mono: "Geist Mono", "ui-monospace", "SFMono-Regular", "Roboto Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dark (Rich warm dark — not pure black) ── */
|
/* ── Dark (Neutral zinc-dark — no blue, layered depth) ── */
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--color-background: #0d0d0d;
|
--color-background: #09090b;
|
||||||
--color-foreground: #ececec;
|
--color-foreground: #e4e4e7;
|
||||||
--color-card: #141414;
|
--color-card: #111113;
|
||||||
--color-card-foreground: #ececec;
|
--color-card-foreground: #e4e4e7;
|
||||||
--color-popover: #1a1a1a;
|
--color-popover: #141416;
|
||||||
--color-popover-foreground: #ececec;
|
--color-popover-foreground: #e4e4e7;
|
||||||
--color-primary: #ececec;
|
--color-primary: #e4e4e7;
|
||||||
--color-primary-foreground: #0d0d0d;
|
--color-primary-foreground: #09090b;
|
||||||
--color-secondary: #1a1a1a;
|
--color-secondary: #1a1a1c;
|
||||||
--color-secondary-foreground: #ececec;
|
--color-secondary-foreground: #e4e4e7;
|
||||||
--color-muted: #1a1a1a;
|
--color-muted: #18181b;
|
||||||
--color-muted-foreground: #888888;
|
--color-muted-foreground: #71717a;
|
||||||
--color-accent: #1a1a1a;
|
--color-accent: #1f1f22;
|
||||||
--color-accent-foreground: #ececec;
|
--color-accent-foreground: #e4e4e7;
|
||||||
--color-destructive: #f43f5e;
|
--color-destructive: #f43f5e;
|
||||||
--color-destructive-foreground: #ececec;
|
--color-destructive-foreground: #e4e4e7;
|
||||||
--color-border: #262626;
|
--color-border: #27272a;
|
||||||
--color-ring: #3b82f6;
|
--color-ring: #71717a;
|
||||||
--color-signal: #f43f5e;
|
--color-signal: #f43f5e;
|
||||||
--color-input: #262626;
|
--color-input: #27272a;
|
||||||
--color-overlay: #050505;
|
--color-overlay: #050505;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ body {
|
|||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
transform: scale(1.03);
|
transform: scale(1.03);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0px 0px 0px 2px var(--color-ring),
|
0px 0px 0px 2px var(--color-muted-foreground),
|
||||||
0px 12px 32px rgba(0, 0, 0, 0.25);
|
0px 12px 32px rgba(0, 0, 0, 0.25);
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -4px;
|
inset: -4px;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
border: 2px dashed var(--color-ring);
|
border: 2px dashed var(--color-muted-foreground);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -154,8 +154,8 @@ body {
|
|||||||
.drop-target-line {
|
.drop-target-line {
|
||||||
height: 3px;
|
height: 3px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: var(--color-ring);
|
background: var(--color-muted-foreground);
|
||||||
box-shadow: 0 0 8px var(--color-ring);
|
box-shadow: 0 0 8px var(--color-muted-foreground);
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
animation: pulse-line 1.2s ease-in-out infinite;
|
animation: pulse-line 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -181,14 +181,14 @@ body {
|
|||||||
|
|
||||||
/* ── Colorful badge variants ── */
|
/* ── Colorful badge variants ── */
|
||||||
.badge-local {
|
.badge-local {
|
||||||
background: #0f291e;
|
background: #0f1a15;
|
||||||
color: #34d399;
|
color: #34d399;
|
||||||
}
|
}
|
||||||
.badge-external {
|
.badge-external {
|
||||||
background: #162038;
|
background: #1a1a1c;
|
||||||
color: #60a5fa;
|
color: #a1a1aa;
|
||||||
}
|
}
|
||||||
.badge-custom {
|
.badge-custom {
|
||||||
background: #231a38;
|
background: #1a1a1c;
|
||||||
color: #a78bfa;
|
color: #a1a1aa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function AddAppTile({ onClick }: { onClick: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="service-card group flex aspect-square flex-col items-center justify-center gap-2.5 rounded-[24px] border border-dashed border-border bg-card p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-accent hover:border-ring/40 hover:shadow-border-hover"
|
className="service-card group flex aspect-square flex-col items-center justify-center gap-2.5 rounded-[24px] border border-dashed border-border bg-card p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-accent hover:border-muted-foreground/40 hover:shadow-border-hover"
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-secondary transition-colors group-hover:bg-accent">
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-secondary transition-colors group-hover:bg-accent">
|
||||||
<Plus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-foreground" />
|
<Plus className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||||
@@ -128,7 +128,7 @@ function DashboardDragOverlay({ activeId, dashboard }: { activeId: string; dashb
|
|||||||
|
|
||||||
if (widget) {
|
if (widget) {
|
||||||
return (
|
return (
|
||||||
<div className="drag-overlay flex w-56 items-center gap-3 rounded-xl bg-card border border-ring/50 px-4 py-3 shadow-2xl">
|
<div className="drag-overlay flex w-56 items-center gap-3 rounded-xl bg-card border border-muted-foreground/40 px-4 py-3 shadow-2xl">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent">
|
||||||
<GripVertical className="h-4 w-4 text-accent-foreground" />
|
<GripVertical className="h-4 w-4 text-accent-foreground" />
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +216,7 @@ export default function DashboardPage() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-background">
|
<div className="flex h-screen flex-col bg-background">
|
||||||
<div className="h-14 border-b border-border/50" />
|
<div className="h-14 border-b border-border" />
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent">
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent">
|
||||||
@@ -261,7 +261,7 @@ export default function DashboardPage() {
|
|||||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
|
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-6 py-32">
|
<div className="flex flex-col items-center justify-center gap-6 py-32">
|
||||||
<div className="flex h-20 w-20 items-center justify-center rounded-[24px] bg-gradient-to-br from-secondary to-accent border border-border shadow-border-card">
|
<div className="flex h-20 w-20 items-center justify-center rounded-[24px] bg-secondary border border-border shadow-border-card">
|
||||||
<LayoutGrid className="h-8 w-8 text-muted-foreground" />
|
<LayoutGrid className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -290,7 +290,7 @@ export default function DashboardPage() {
|
|||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-4 w-0.5 rounded-full bg-ring" />
|
<div className="h-4 w-0.5 rounded-full bg-muted-foreground" />
|
||||||
<CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Widgets</CardTitle>
|
<CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Widgets</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={openAddWidget} className="gap-1.5 text-xs rounded-lg hover:bg-accent">
|
<Button variant="ghost" size="sm" onClick={openAddWidget} className="gap-1.5 text-xs rounded-lg hover:bg-accent">
|
||||||
@@ -310,7 +310,7 @@ export default function DashboardPage() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={openAddWidget}
|
onClick={openAddWidget}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-secondary/50 p-6 text-sm text-muted-foreground transition-all hover:border-ring/40 hover:bg-accent hover:text-foreground"
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-muted p-6 text-sm text-muted-foreground transition-all hover:border-muted-foreground/40 hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" /> Add your first widget
|
<Plus className="h-4 w-4" /> Add your first widget
|
||||||
</button>
|
</button>
|
||||||
@@ -322,7 +322,7 @@ export default function DashboardPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-4 w-0.5 rounded-full bg-ring" />
|
<div className="h-4 w-0.5 rounded-full bg-muted-foreground" />
|
||||||
<CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Apps</CardTitle>
|
<CardTitle className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Apps</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -370,7 +370,7 @@ export default function DashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
{groups.length > 0 && (
|
{groups.length > 0 && (
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<div className="h-4 w-0.5 rounded-full bg-ring" />
|
<div className="h-4 w-0.5 rounded-full bg-muted-foreground" />
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Ungrouped</span>
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Ungrouped</span>
|
||||||
<span className="text-xs text-muted-foreground font-mono">{ungrouped.length}</span>
|
<span className="text-xs text-muted-foreground font-mono">{ungrouped.length}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,7 +401,7 @@ export default function DashboardPage() {
|
|||||||
{groups.length === 0 && ungrouped.length === 0 && (
|
{groups.length === 0 && ungrouped.length === 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={openAddService}
|
onClick={openAddService}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-secondary/50 p-8 text-sm text-muted-foreground transition-all hover:border-ring/40 hover:bg-accent hover:text-foreground"
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-muted p-8 text-sm text-muted-foreground transition-all hover:border-muted-foreground/40 hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" /> Add your first app
|
<Plus className="h-4 w-4" /> Add your first app
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export function GroupSection({ group, onEditService, onDeleteService, onEditGrou
|
|||||||
className="flex flex-1 items-center gap-2.5 group/title min-w-0"
|
className="flex flex-1 items-center gap-2.5 group/title min-w-0"
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg transition-colors bg-accent">
|
<div className="flex h-7 w-7 items-center justify-center rounded-lg transition-colors bg-secondary">
|
||||||
<FolderOpen className="h-3.5 w-3.5 text-accent-foreground" />
|
<FolderOpen className="h-3.5 w-3.5 text-secondary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<span className="text-sm font-semibold truncate">{group.name}</span>
|
<span className="text-sm font-semibold truncate">{group.name}</span>
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ export function ServiceCard({
|
|||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{/* Gradient accent line at top */}
|
{/* Accent line at top */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-[2px] opacity-0 group-hover:opacity-100 transition-opacity bg-ring" />
|
<div className="absolute top-0 left-0 right-0 h-[2px] opacity-0 group-hover:opacity-100 transition-opacity bg-muted-foreground" />
|
||||||
|
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-2.5 p-4">
|
<div className="flex h-full flex-col items-center justify-center gap-2.5 p-4">
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function Header({
|
|||||||
const dateStr = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
|
const dateStr = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 w-full border-b border-border bg-background">
|
<header className="sticky top-0 z-40 w-full border-b border-border bg-card">
|
||||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
|
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-9 w-full rounded-md border border-border bg-card px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-muted file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ export function WidgetCard({
|
|||||||
const typeIcon = widgetTypeIcons[widget.type] || <Activity className="h-3.5 w-3.5" />;
|
const typeIcon = widgetTypeIcons[widget.type] || <Activity className="h-3.5 w-3.5" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative border-0 overflow-hidden rounded-2xl shadow-[0px_0px_0px_1px_var(--color-border)] hover:shadow-border-hover transition-all duration-200">
|
<Card className="group relative overflow-hidden rounded-2xl border border-border bg-card hover:shadow-border-hover transition-all duration-200">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute top-0 left-0 right-0 h-1 opacity-60 bg-ring"
|
"absolute top-0 left-0 right-0 h-0.5 opacity-40 bg-muted-foreground"
|
||||||
)} />
|
)} />
|
||||||
<CardHeader className="flex flex-row items-center justify-between pt-4 pb-2 px-4">
|
<CardHeader className="flex flex-row items-center justify-between pt-4 pb-2 px-4">
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
@@ -156,7 +156,7 @@ function ImageContent({ config }: { config: Record<string, unknown> }) {
|
|||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Widget image"
|
alt="Widget image"
|
||||||
className="max-h-48 w-full rounded-xl object-cover border border-border/20 shadow-sm"
|
className="max-h-48 w-full rounded-xl object-cover border border-border shadow-sm"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = "none";
|
(e.target as HTMLImageElement).style.display = "none";
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user