Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/app/dashboard/[guildId]/forms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const dynamic = "force-dynamic";

const FORM_STATUS_COLORS: Record<string, string> = {
draft: "#6b6b72",
live: "#00E676",
live: "#5eb131",
closed: "#f5a623",
archived: "#6b6b72",
};
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default async function GuildsPage() {
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{guilds.map((guild) => (
<Link key={guild.id} href={`/dashboard/${guild.id}/forms` as Route} className="group">
<Card className="flex h-full flex-col gap-4 p-5 transition-all group-hover:-translate-y-0.5 group-hover:border-primary/40 group-hover:shadow-md">
<Card className="flex h-full flex-col gap-4 p-5 transition-all group-hover:-translate-y-0.5 group-hover:border-primary/40 group-hover:shadow-card-hover">
<div className="flex items-center gap-3">
{guild.icon ? (
<img src={guild.icon} alt="" width={44} height={44} className="rounded-lg" />
Expand Down
87 changes: 83 additions & 4 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
--card-foreground: 0 0% 10%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 10%;
--primary: 161 94% 30%;
--primary: 101 62% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 93%;
--secondary-foreground: 0 0% 10%;
Expand All @@ -23,7 +23,7 @@
--destructive-foreground: 0 0% 100%;
--border: 0 0% 88%;
--input: 0 0% 88%;
--ring: 161 94% 30%;
--ring: 101 62% 40%;
--chart-1: 43 96% 56%;
--chart-2: 161 64% 42%;
--chart-3: 200 80% 47%;
Expand All @@ -39,7 +39,7 @@
--card-foreground: 0 0% 97%;
--popover: 0 0% 12%;
--popover-foreground: 0 0% 97%;
--primary: 161 84% 38%;
--primary: 99 57% 44%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 17%;
--secondary-foreground: 0 0% 97%;
Expand All @@ -51,7 +51,7 @@
--destructive-foreground: 0 0% 98%;
--border: 0 0% 20%;
--input: 0 0% 23%;
--ring: 161 84% 42%;
--ring: 99 57% 44%;
--chart-1: 43 96% 56%;
--chart-2: 161 64% 50%;
--chart-3: 200 80% 55%;
Expand All @@ -76,3 +76,82 @@
@apply bg-primary/20;
}
}

/* ============================================================================
Polish patterns — aligned with the MSK ecosystem (msk-shop / paste / shortener)
============================================================================ */

/* Eyebrow label — small MSK-green mono kicker above section headlines. */
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-mono), ui-monospace, monospace;
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: hsl(var(--primary));
}
.eyebrow::before {
content: "";
display: block;
width: 24px;
height: 1px;
background: hsl(var(--primary));
}

/* Radial accent gradient behind hero sections (CSP-safe, no inline styles). */
.hero-decor-gradient {
background: radial-gradient(
ellipse 60% 50% at 30% 20%,
hsl(var(--primary) / 0.12),
transparent 70%
);
}

/* Pulsing live-indicator dot. */
.pulse-dot {
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.85);
}
}

/* Custom scrollbars (pointer devices only). */
@media (pointer: fine) {
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: hsl(var(--muted));
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground));
}
}

/* Respect reduced-motion preferences. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
22 changes: 8 additions & 14 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Noto_Sans, Outfit, Space_Mono } from "next/font/google";
import { Inter, JetBrains_Mono } from "next/font/google";
import { headers } from "next/headers";

import { SiteFooter } from "@/components/site-footer";
Expand All @@ -11,22 +11,16 @@ import { getGuildByDomain, isPrimaryHostname, requestHostname } from "@/lib/cust
import { getDirection, getLocale } from "@/i18n";
import "./globals.css";

const outfit = Outfit({
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-outfit",
weight: ["400", "500", "600", "700", "800"],
variable: "--font-inter",
display: "swap",
});
const notoSans = Noto_Sans({
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
weight: ["600", "700", "800"],
variable: "--font-noto-sans",
display: "swap",
});
const spaceMono = Space_Mono({
subsets: ["latin"],
weight: ["400", "700"],
variable: "--font-space-mono",
weight: ["400", "500", "700"],
variable: "--font-jetbrains-mono",
display: "swap",
});

Expand Down Expand Up @@ -74,7 +68,7 @@ export default async function RootLayout({
lang={locale}
dir={getDirection(locale)}
suppressHydrationWarning
className={`${outfit.variable} ${notoSans.variable} ${spaceMono.variable}`}
className={`${inter.variable} ${jetbrainsMono.variable}`}
>
<body className="font-sans antialiased">
<ThemeProvider nonce={nonce}>
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/app/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export default async function PricingPage() {
return (
<main className="container py-16 lg:py-24">
<div className="mx-auto max-w-2xl text-center">
<h1 className="text-balance font-heading text-4xl font-bold tracking-tight md:text-5xl">
<span className="eyebrow justify-center">{t.eyebrow}</span>
<h1 className="mt-4 text-balance font-heading text-4xl font-bold tracking-tight md:text-5xl">
{t.title}
</h1>
<p className="mt-4 text-pretty text-lg leading-relaxed text-muted-foreground">{t.sub}</p>
Expand All @@ -100,7 +101,7 @@ export default async function PricingPage() {
className={
highlight
? "relative border-primary shadow-lg shadow-primary/10 lg:-mt-3"
: "relative"
: "relative transition hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-card-hover"
}
>
{highlight && (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/branding/branding-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Dictionary } from "@/i18n";

type BrandingDict = Dictionary["branding"];

const DEFAULT_ACCENT = "#00e676";
const DEFAULT_ACCENT = "#5eb131";
const HEX = /^#[0-9a-fA-F]{6}$/;

export function BrandingForm({
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/landing/feature-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export async function FeatureGrid() {
return (
<section className="container py-20 lg:py-24">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-balance font-heading text-3xl font-bold tracking-tight md:text-4xl">
<span className="eyebrow justify-center">{t.eyebrow}</span>
<h2 className="mt-4 text-balance font-heading text-3xl font-bold tracking-tight md:text-4xl">
{t.title}
</h2>
<p className="mt-4 text-pretty text-lg leading-relaxed text-muted-foreground">{t.sub}</p>
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/components/landing/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export async function Features() {
return (
<section className="container py-20 lg:py-28">
<div className="max-w-2xl">
<h2 className="text-balance font-heading text-3xl font-bold tracking-tight md:text-4xl">
<span className="eyebrow">{t.features.eyebrow}</span>
<h2 className="mt-4 text-balance font-heading text-3xl font-bold tracking-tight md:text-4xl">
{t.features.title}
</h2>
<p className="mt-4 text-pretty text-lg leading-relaxed text-muted-foreground">
Expand All @@ -19,7 +20,7 @@ export async function Features() {
</div>

<div className="mt-12 grid gap-5 lg:grid-cols-3 lg:grid-rows-2">
<Card className="flex flex-col justify-between lg:col-span-2 lg:row-span-2">
<Card className="flex flex-col justify-between transition hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-card-hover lg:col-span-2 lg:row-span-2">
<CardContent className="flex h-full flex-col justify-between gap-10 p-8">
<div>
<FeatureIcon>
Expand Down Expand Up @@ -67,7 +68,7 @@ function FeatureCard({
body: string;
}) {
return (
<Card>
<Card className="transition hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-card-hover">
<CardContent className="p-7">
<FeatureIcon>{icon}</FeatureIcon>
<h3 className="mt-6 font-heading text-xl font-bold">{title}</h3>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export async function Hero({

return (
<section className="relative overflow-hidden">
<div aria-hidden className="hero-decor-gradient pointer-events-none absolute inset-0 -z-10" />
<div className="container grid items-center gap-12 pb-20 pt-16 lg:grid-cols-[1.05fr_0.95fr] lg:pb-28 lg:pt-24">
<div>
<Badge variant="outline" className="gap-1.5 border-primary/30 text-primary">
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
<span className="pulse-dot h-1.5 w-1.5 rounded-full bg-primary" />
{t.hero.badge}
</Badge>

Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/landing/steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export async function Steps() {
return (
<section className="border-y border-border bg-muted/30">
<div className="container py-20 lg:py-24">
<h2 className="text-balance font-heading text-3xl font-bold tracking-tight md:text-4xl">
<span className="eyebrow">{t.steps.eyebrow}</span>
<h2 className="mt-4 text-balance font-heading text-3xl font-bold tracking-tight md:text-4xl">
{t.steps.title}
</h2>

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
className={cn("rounded-lg border bg-card text-card-foreground shadow-card transition-shadow", className)}
{...props}
/>
),
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/i18n/dictionaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const en = {
inviteBot: "Invite the bot",
},
features: {
eyebrow: "Why MSK Forms",
title: "The part most form tools skip.",
sub: "A form is easy. Telling people what happened after they hit submit is the hard part. That is the whole point of MSK Forms.",
uspTitle: "Applicants always know where they stand.",
Expand All @@ -29,6 +30,7 @@ const en = {
buildBody: "Compose forms from typed fields: text, choices, dates, consent. Ready in seconds.",
},
featureGrid: {
eyebrow: "Feature set",
title: "Everything you need to run applications",
sub: "From the form to the decision — and the applicant follows along live.",
items: [
Expand All @@ -51,6 +53,7 @@ const en = {
],
},
steps: {
eyebrow: "How it works",
title: "From zero to live in three steps.",
build: "Build", buildBody: "Drop in the fields you need and set who can submit.",
share: "Share", shareBody: "Publish a link or post the form to Discord with the bot.",
Expand All @@ -63,6 +66,7 @@ const en = {
},
pricing: {
nav: "Pricing",
eyebrow: "Plans",
title: "Pricing that scales with your server.",
sub: "Start free. Upgrade a guild whenever it outgrows the free limits.",
perMonth: "/month",
Expand Down Expand Up @@ -490,6 +494,7 @@ const de: Dictionary = {
inviteBot: "Bot einladen",
},
features: {
eyebrow: "Warum MSK Forms",
title: "Der Teil, den die meisten Formular-Tools auslassen.",
sub: "Ein Formular ist leicht. Den Leuten zu sagen, was nach dem Absenden passiert, ist der schwere Teil. Genau darum geht es bei MSK Forms.",
uspTitle: "Bewerber wissen immer, woran sie sind.",
Expand All @@ -502,6 +507,7 @@ const de: Dictionary = {
buildBody: "Formulare aus typisierten Feldern zusammenstellen: Text, Auswahl, Datum, Zustimmung. In Sekunden fertig.",
},
featureGrid: {
eyebrow: "Funktionsumfang",
title: "Alles, um Bewerbungen zu betreiben",
sub: "Vom Formular bis zur Entscheidung — und der Bewerber verfolgt alles live mit.",
items: [
Expand All @@ -524,6 +530,7 @@ const de: Dictionary = {
],
},
steps: {
eyebrow: "So funktioniert's",
title: "In drei Schritten live.",
build: "Bauen", buildBody: "Felder reinziehen und festlegen, wer einreichen darf.",
share: "Teilen", shareBody: "Link veröffentlichen oder das Formular per Bot in Discord posten.",
Expand All @@ -536,6 +543,7 @@ const de: Dictionary = {
},
pricing: {
nav: "Preise",
eyebrow: "Tarife",
title: "Preise, die mit deinem Server mitwachsen.",
sub: "Kostenlos starten. Eine Guild upgraden, sobald sie die Free-Grenzen sprengt.",
perMonth: "/Monat",
Expand Down Expand Up @@ -959,6 +967,7 @@ const hu: Dictionary = {
inviteBot: "Bot meghívása",
},
features: {
eyebrow: "Miért MSK Forms",
title: "Amit a legtöbb űrlapeszköz kihagy.",
sub: "Egy űrlap egyszerű. A nehéz rész elmondani az embereknek, mi történt a beküldés után. Pontosan erről szól a MSK Forms.",
uspTitle: "A jelentkezők mindig tudják, hol tartanak.",
Expand All @@ -971,6 +980,7 @@ const hu: Dictionary = {
buildBody: "Állítsd össze az űrlapokat típusos mezőkből: szöveg, választás, dátum, hozzájárulás. Másodpercek alatt kész.",
},
featureGrid: {
eyebrow: "Funkciók",
title: "Minden, ami a jelentkezésekhez kell",
sub: "Az űrlaptól a döntésig — a jelentkező pedig élőben követi.",
items: [
Expand All @@ -993,6 +1003,7 @@ const hu: Dictionary = {
],
},
steps: {
eyebrow: "Hogyan működik",
title: "Nulláról élesbe három lépésben.",
build: "Építés", buildBody: "Húzd be a szükséges mezőket, és állítsd be, ki küldhet be.",
share: "Megosztás", shareBody: "Tedd közzé a linket, vagy posztold az űrlapot Discordra a bottal.",
Expand All @@ -1005,6 +1016,7 @@ const hu: Dictionary = {
},
pricing: {
nav: "Árak",
eyebrow: "Csomagok",
title: "Árazás, amely együtt nő a szervereddel.",
sub: "Kezdj ingyen. Frissíts egy szervert, amint kinövi az ingyenes korlátokat.",
perMonth: "/hó",
Expand Down
Loading