Files
AI_portal/landing/src/components/tile.tsx
2026-07-07 13:16:26 +02:00

167 lines
5.9 KiB
TypeScript

"use client";
import * as Icons from "lucide-react";
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
export type AppTile = {
id: string;
title: string;
description: string;
category: string;
icon: string;
accent: string;
href: string;
featured?: boolean;
/** Open in a new tab. Defaults to false — task-style apps replace the
current tab so the browser back button works naturally. Set true for
long-running sessions like the chat tile. */
newTab?: boolean;
/** Visual kind. "tool" (default) is a normal app tile. "cta" renders
a dashed-border invitation card visually distinct from the tools. */
kind?: "tool" | "cta";
};
const accentVar = (accent: string) => `var(--color-accent-${accent})`;
export function Tile({ app }: { app: AppTile }) {
const { resolvedTheme } = useTheme();
const LucideIcon =
(Icons[app.icon as keyof typeof Icons] as React.ComponentType<{
className?: string;
}>) ?? Icons.Sparkles;
const isExternal = app.href.startsWith("http");
const isDisabled = app.href === "#";
// Append current theme to internal-app URLs so they pick up the same scheme.
const isInternalApp = isExternal && app.href.includes(".klas.chat");
const hrefWithTheme = (() => {
if (!isInternalApp || !resolvedTheme) return app.href;
try {
const u = new URL(app.href);
u.searchParams.set("theme", resolvedTheme);
return u.toString();
} catch {
return app.href;
}
})();
const isCta = app.kind === "cta";
const content = isCta ? (
// ─── CTA / "request a new tool" — visually distinct ───
<div
className="group relative flex h-full flex-col items-center justify-center gap-3 overflow-hidden rounded-lg border border-dashed border-[var(--color-border-hover)] bg-transparent p-6 text-center transition-all duration-200 ease-in-out hover:border-solid hover:bg-[var(--color-card-hover)]"
style={{ borderColor: `color-mix(in srgb, ${accentVar(app.accent)} 35%, var(--color-border-hover))` }}
>
<div
className="flex h-12 w-12 items-center justify-center rounded-full"
style={{
background: `color-mix(in srgb, ${accentVar(app.accent)} 12%, transparent)`,
color: accentVar(app.accent),
}}
>
<LucideIcon className="h-6 w-6" />
</div>
<div>
<h3 className="system-sm-semibold text-[var(--color-text-primary)]">
{app.title}
</h3>
<p className="system-xs-regular mt-1.5 text-[var(--color-text-tertiary)]">
{app.description}
</p>
</div>
</div>
) : (
// ─── Standard tool tile ───
<div
className={cn(
"group relative flex h-full flex-col overflow-hidden rounded-lg border-[0.5px] border-[var(--color-border-default)] bg-[var(--color-card)] shadow-[var(--shadow-card)] transition-all duration-200 ease-in-out",
!isDisabled &&
"hover:bg-[var(--color-card-hover)] hover:shadow-[var(--shadow-card-hover)] hover:border-[var(--color-border-hover)]",
isDisabled && "opacity-70",
)}
>
<div className="flex items-start gap-3 px-[14px] pt-[14px] pb-3">
<div
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[10px]"
style={{
background: `color-mix(in srgb, ${accentVar(app.accent)} 12%, transparent)`,
color: accentVar(app.accent),
}}
>
<LucideIcon className="h-5 w-5" />
</div>
<div className="min-w-0 grow py-px">
<div className="flex items-start justify-between gap-2">
<h3 className="system-sm-semibold truncate text-[var(--color-text-secondary)]">
{app.title}
</h3>
{!isDisabled && (
<ArrowUpRight className="mt-0.5 h-4 w-4 shrink-0 text-[var(--color-text-quaternary)] opacity-0 transition-all group-hover:opacity-100" />
)}
</div>
<div className="system-2xs-medium-uppercase mt-0.5 truncate text-[var(--color-text-tertiary)]">
{app.category}
</div>
</div>
</div>
<div className="px-[14px] pb-3">
<p className="system-xs-regular line-clamp-3 text-[var(--color-text-tertiary)]">
{app.description}
</p>
</div>
{isDisabled && (
<div className="px-[14px] pb-[14px]">
<span className="system-2xs-medium-uppercase inline-flex items-center rounded-full bg-[var(--color-bg-tertiary)] px-2 py-1 text-[var(--color-text-tertiary)]">
Brzy k dispozici
</span>
</div>
)}
{!isDisabled && app.featured && (
<div className="mt-auto px-[14px] pb-[14px]">
<span
className="system-2xs-medium-uppercase inline-flex items-center rounded-full px-2 py-1 text-[var(--color-primary-foreground)]"
style={{ background: accentVar(app.accent) }}
>
Doporučeno
</span>
</div>
)}
</div>
);
if (isDisabled) {
return <div className="h-full">{content}</div>;
}
if (isExternal) {
const inNewTab = app.newTab === true;
return (
<a
href={hrefWithTheme}
target={inNewTab ? "_blank" : "_self"}
rel={inNewTab ? "noopener noreferrer" : undefined}
className="h-full rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-secondary)]"
>
{content}
</a>
);
}
return (
<Link
href={app.href}
className="h-full rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-secondary)]"
>
{content}
</Link>
);
}