diff --git a/apps/site/src/app/support/page.tsx b/apps/site/src/app/support/page.tsx index 7a22cd9f4e2..d6d4be73376 100644 --- a/apps/site/src/app/support/page.tsx +++ b/apps/site/src/app/support/page.tsx @@ -1,156 +1,333 @@ +import type { ReactNode } from "react"; import { createPageMetadata } from "@/lib/page-metadata"; -import { ArrowRight } from "@/components/icons/forma"; -import { RoleKicker } from "@/components/brand/role-kicker"; +import { cn } from "@/lib/utils"; import { Texture } from "@/components/brand/texture"; +import { SupportSearch } from "@/components/support/support-search"; +import { Reveal } from "@/components/support/reveal"; +import { + ArrowRight, + CheckCircle, + ConsoleCode, + Discord, + DocWindow, + Github, + Mail, + PolicyTable, + Shield, +} from "@/components/support/support-icons"; +import "./support.css"; export const metadata = createPageMetadata({ title: "Prisma Support | Get Help, Report Bugs, and Request Features", description: - "Get help with Prisma. Search for answers, report bugs, request features, or contact the Prisma support team.", + "Find an answer in the docs, ask the community, or reach our support team. Where you go depends on your plan and what you need.", path: "/support", ogKicker: "Support", }); -// Ported from the old site's /support page (apps/site + data/support.json), -// rebuilt in the redesign language: one card per support path, accent bars -// instead of the old gradient washes. +// Ported from the approved support design. Visual styling is reproduced +// faithfully; link targets use the live site's real routes. -type SupportCard = { - title: string; - description: string; - accent: string; - links: { label: string; url: string }[]; -}; +const RAINBOW = + "linear-gradient(85deg, #01d7e4 0%, #f3c306 25%, #f37a03 50%, #f43531 74%, #f00e5c 100%)"; -const SUPPORT_CARDS: SupportCard[] = [ +const HERO_GLOW = + "radial-gradient(52% 40% at 30% 100%, color-mix(in srgb, var(--color-prism-cyan-400) 34%, transparent), transparent 68%),radial-gradient(44% 36% at 52% 100%, color-mix(in srgb, var(--color-prism-yellow-300) 26%, transparent), transparent 66%),radial-gradient(42% 30% at 74% 100%, color-mix(in srgb, var(--color-prism-red-400) 28%, transparent), transparent 68%)"; + +const PANEL_GLOW = + "radial-gradient(52% 44% at 28% 100%, color-mix(in srgb, var(--color-prism-cyan-400) 32%, transparent), transparent 70%),radial-gradient(44% 38% at 52% 100%, color-mix(in srgb, var(--color-prism-yellow-300) 24%, transparent), transparent 66%),radial-gradient(48% 40% at 74% 100%, color-mix(in srgb, var(--color-prism-red-400) 28%, transparent), transparent 70%)"; + +const ICON_GLOW = + "radial-gradient(80% 55% at 20% 100%, color-mix(in srgb, var(--color-prism-cyan-300) 45%, transparent), transparent 70%),radial-gradient(70% 50% at 52% 100%, color-mix(in srgb, var(--color-prism-yellow-300) 40%, transparent), transparent 68%),radial-gradient(75% 52% at 84% 100%, color-mix(in srgb, var(--color-prism-red-300) 42%, transparent), transparent 70%)"; + +const outlineButton = + "spectrum-border flex items-center justify-center rounded-full border border-[#646567] cursor-pointer transition-colors duration-500 hover:border-transparent px-[22px] py-[11px] text-[16px]"; +const outlineButtonLabel = "whitespace-nowrap font-semibold leading-[1.5] text-foreground"; + +function GlowIcon({ className, children }: { className?: string; children: ReactNode }) { + return ( + + + {children} + + ); +} + +// Solid black pill button with a soft rainbow bloom behind it. +function PrimaryButton({ href, children }: { href: string; children: ReactNode }) { + return ( + + + + + + + {children} + + + + ); +} + +const SUPPORT_CARDS = [ { - title: "Issues & feature requests", - description: "Found a bug, or want to request something new? Let us know.", - accent: "bg-prism-cyan-400/70", - links: [ - { - label: "Report a bug", - url: "https://github.com/prisma/orm/issues/new?assignees=&labels=&template=bug_report.md", - }, - { - label: "Request a new feature", - url: "https://github.com/prisma/orm/issues/new?assignees=&labels=&template=feature_request.md", - }, - ], + title: "Community support", + description: + "Ask questions and share what you're building with thousands of developers on our Discord. Available on every plan, including the free tier.", + icon: , + cta: { label: "Join our Discord", url: "https://pris.ly/discord" }, }, { - title: "Starter plan", + title: "Direct support", description: - "Support for customers on our Starter plan is provided through our community channels.", - accent: "bg-prism-yellow-300", - links: [{ label: "Join our Discord channel", url: "https://pris.ly/discord" }], + "On a Pro or Business plan? Submit a ticket from your Console, and our team will help.", + icon: , + cta: { label: "Submit a ticket", url: "https://console.prisma.io" }, }, { - title: "Pro & Business plans", + title: "Enterprise support", description: - "Support for customers on our Pro or Business plan is provided through the Platform Console.", - accent: "bg-prism-red-400/80", - links: [{ label: "Submit a ticket", url: "https://console.prisma.io" }], + "Need higher-touch support with guaranteed response times? Explore our enterprise options.", + icon: , + cta: { label: "Discover Enterprise support", url: "/enterprise" }, }, ]; -const MORE_LINKS = [ - { label: "Documentation", url: "/docs" }, - { label: "Prisma examples", url: "https://github.com/prisma/prisma-examples" }, - { label: "Support policy", url: "/support-policy" }, - { label: "Enterprise support", url: "/enterprise" }, +const RESOURCES = [ + { + title: "Documentation", + description: "Guides, references, and API docs for every part of Prisma.", + url: "/docs", + cta: "Read the docs", + icon: , + }, + { + title: "Examples", + description: "Ready-to-run example projects on GitHub.", + url: "https://github.com/prisma/prisma-examples", + cta: "Browse examples", + icon: , + }, + { + title: "Platform status", + description: "Check current status and past incidents.", + url: "https://www.prisma-status.com", + cta: "View status", + icon: , + }, + { + title: "Support policy", + description: "What each plan includes and how support works.", + url: "/support-policy", + cta: "Read the policy", + icon: , + }, ]; export default function SupportPage() { return ( - <> +
+ {/* Hero */}
+
+
+
+
+
+
+
-
- - Support - -

- How can we help? +
+

+ How can we{" "} + + help + + + + + + + ?

-

- Search the docs, report bugs, request features, or reach the Prisma support team. +

+ Find an answer in the docs, ask the community, or reach our support team. Where you + go depends on your plan and what you need.

+

-
-
-
+ {/* Ways to get support */} +
+
+ +

+ Ways to get support +

+
+
{SUPPORT_CARDS.map((card) => ( -
- -

{card.title}

-

{card.description}

- + ))}
+ +

+ Response times depend on your subscription plan and current volume. +

+
+
+
-
-
-
-

Still need help?

-

- We're here to help. Response times depend on your subscription level and the - volume of requests we're receiving. -

-
- +
+ +
+
- Email us - - +
+
+
+
+
+ +
+ + + +
+

+ + Report an issue +

+

+ Found a bug or want a feature? +

+

+ Report bugs and request features on GitHub, where our team and the community + track them in the open. +

+
+
+ + Report a bug + + + Request a feature + +
+
-
- {MORE_LINKS.map((link) => ( + +
+
+ + {/* Resources */} +
+
+ +

+ Resources for faster answers +

+
+ + -
+ +
+ + + {/* Still stuck */} +
+
+ +

+ Still stuck? +

+

+ If you can't find what you need, email us and we'll point you in the right + direction. +

+
+ + + + + + Email support@prisma.io + + + +
- + ); } diff --git a/apps/site/src/app/support/support.css b/apps/site/src/app/support/support.css new file mode 100644 index 00000000000..c88f6c2b881 --- /dev/null +++ b/apps/site/src/app/support/support.css @@ -0,0 +1,106 @@ +/* Support page motion + spectrum border — ported from the approved design. */ + +/* Animated rainbow border, revealed on hover / focus-within. */ +.spectrum-border { + position: relative; +} +.spectrum-border::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background-image: linear-gradient(85deg, #01d7e4, #f3c306, #f37a03, #f43531, #f00e5c, #01d7e4); + background-size: 200% 100%; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s; + animation: 2.5s linear infinite spectrum-slide; + -webkit-mask-image: linear-gradient(#fff 0 0), linear-gradient(#fff 0 0); + mask-image: linear-gradient(#fff 0 0), linear-gradient(#fff 0 0); + -webkit-mask-clip: content-box, border-box; + mask-clip: content-box, border-box; + -webkit-mask-composite: xor; + mask-composite: exclude; +} +.spectrum-border:hover::before, +.spectrum-border-on::before { + opacity: 1; +} +.spectrum-border-focus:focus-within::before { + opacity: 1; +} + +@keyframes spectrum-slide { + 0% { + background-position: 0%; + } + 100% { + background-position: 200%; + } +} + +/* Hero entrance + glass glide sweep. */ +.animate-hero-rise { + animation: 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.45s both hero-rise; +} +.animate-glass-glide { + animation: 9s 1.8s infinite both glass-glide; +} + +@keyframes hero-rise { + 0% { + opacity: 0; + transform: translateY(16px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +@keyframes glass-glide { + 0% { + opacity: 0; + animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); + left: -20%; + } + 5% { + opacity: 1; + } + 25% { + opacity: 1; + } + 30%, + 100% { + opacity: 0; + left: 80%; + } +} + +/* Scroll-reveal (opacity + lift), toggled via data-shown by the Reveal component. */ +.reveal { + opacity: 0; + transform: translateY(44px); + transition: + opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.7s cubic-bezier(0.22, 1, 0.36, 1); +} +.reveal[data-shown="true"] { + opacity: 1; + transform: none; +} + +@media (prefers-reduced-motion: reduce) { + .spectrum-border::before, + .animate-hero-rise, + .animate-glass-glide { + animation: none; + } + /* Honor reduced motion: show content in place, no lift or transition. */ + .reveal, + .reveal[data-shown="true"] { + opacity: 1; + transform: none; + transition: none; + } +} diff --git a/apps/site/src/components/support/reveal.tsx b/apps/site/src/components/support/reveal.tsx new file mode 100644 index 00000000000..d735adcefed --- /dev/null +++ b/apps/site/src/components/support/reveal.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useRef, useState, type ReactNode } from "react"; + +// Scroll-reveal wrapper (opacity + translateY) matching the approved design. +// Falls back to visible when IntersectionObserver is unavailable. +export function Reveal({ children, className }: { children: ReactNode; className?: string }) { + const ref = useRef(null); + const [shown, setShown] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + if (typeof IntersectionObserver === "undefined") { + setShown(true); + return; + } + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setShown(true); + observer.disconnect(); + } + }, + { rootMargin: "0px 0px -10% 0px", threshold: 0.1 }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +} diff --git a/apps/site/src/components/support/support-icons.tsx b/apps/site/src/components/support/support-icons.tsx new file mode 100644 index 00000000000..0015b8f7dff --- /dev/null +++ b/apps/site/src/components/support/support-icons.tsx @@ -0,0 +1,86 @@ +// Support page icon set — ported verbatim from the approved support design. +// 24x24 glyphs; color via currentColor, size/color via the className prop. +import type { SVGProps } from "react"; + +type IconProps = SVGProps; + +export function SearchGlass({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function Discord({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function ConsoleCode({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function Shield({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function Github({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function DocWindow({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function ArrowRight({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function CheckCircle({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function PolicyTable({ className, ...props }: IconProps) { + return ( + + + + ); +} + +export function Mail({ className, ...props }: IconProps) { + return ( + + + + ); +} + diff --git a/apps/site/src/components/support/support-search.tsx b/apps/site/src/components/support/support-search.tsx new file mode 100644 index 00000000000..93c16b6e6e7 --- /dev/null +++ b/apps/site/src/components/support/support-search.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useSearchContext } from "fumadocs-ui/contexts/search"; +import { SearchGlass } from "@/components/support/support-icons"; + +// Hero search box, styled per the approved design, wired to the site-wide +// unified search dialog (the same context the header trigger uses). +export function SupportSearch() { + const { setOpenSearch } = useSearchContext(); + const open = () => setOpenSearch(true); + + return ( +
+
{ + event.preventDefault(); + open(); + }} + className="spectrum-border-focus spectrum-border flex w-full items-center gap-2 rounded-2xl border border-black/[0.09] bg-white p-2 pl-4 shadow-[0_1px_2px_rgba(21,21,21,0.04),0_16px_40px_-24px_rgba(21,21,21,0.22)] transition-colors" + > + + + { + event.preventDefault(); + open(); + }} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + open(); + } + }} + className="min-w-0 flex-1 cursor-pointer bg-transparent text-[15px] leading-6 text-foreground outline-none placeholder:text-muted-foreground" + /> + + + + + + + +
+ ); +}