refact: better landing and login page
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { BackgroundPaths } from "@/components/ui/background-paths";
|
||||
import { FlipWords } from "@/components/shared/flip-words";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
|
||||
interface AuthLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -8,11 +14,39 @@ interface AuthLayoutProps {
|
||||
|
||||
export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||
const user = await getCurrentUser();
|
||||
const t = await getTranslations("Auth");
|
||||
|
||||
if (user) {
|
||||
if (user.role === "ADMIN") redirect("/admin");
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
return <div className="min-h-screen">{children}</div>;
|
||||
// return <div className="min-h-screen">{children}</div>;
|
||||
return (
|
||||
<main className="relative flex h-screen w-full flex-col">
|
||||
<div className="flex-1">
|
||||
<div className="flex min-h-screen w-full">
|
||||
<div className="relative hidden flex-col border-r bg-muted p-16 lg:flex lg:w-1/2">
|
||||
<div className="absolute inset-0 h-full w-full">
|
||||
<BackgroundPaths />
|
||||
</div>
|
||||
<h1 className="z-10 flex items-center gap-3 text-2xl font-semibold duration-1000 animate-in fade-in">
|
||||
<Icons.logo className="size-8" />
|
||||
<Link href="/" style={{ fontFamily: "Bahamas Bold" }}>
|
||||
{siteConfig.name}
|
||||
</Link>
|
||||
</h1>
|
||||
<div className="flex-1" />
|
||||
|
||||
<FlipWords
|
||||
words={[t("description")]}
|
||||
className="mb-4 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full p-6 lg:w-1/2">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ export const metadata: Metadata = {
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations("Auth");
|
||||
return (
|
||||
<div className="container flex h-screen w-screen flex-col items-center justify-center">
|
||||
<Link
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
{/* <Link
|
||||
href="/"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
@@ -29,15 +29,12 @@ export default function LoginPage() {
|
||||
<Icons.chevronLeft className="mr-2 size-4" />
|
||||
{t("Back")}
|
||||
</>
|
||||
</Link>
|
||||
</Link> */}
|
||||
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<Icons.logo className="mx-auto size-12" />
|
||||
<Icons.logo className="mx-auto block size-12 lg:hidden" />
|
||||
<div className="text-2xl font-semibold tracking-tight">
|
||||
<span>{t("Welcome to")}</span>{" "}
|
||||
<span style={{ fontFamily: "Bahamas Bold" }}>
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("Choose your login method to continue")}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
import HeroLanding, { LandingImages } from "@/components/sections/hero-landing";
|
||||
import { PricingSection } from "@/components/sections/pricing";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "WR.DO - Your all-in-one domain services platform",
|
||||
description: "List and manage records.",
|
||||
});
|
||||
|
||||
export default async function IndexPage() {
|
||||
const user = await getCurrentUser();
|
||||
return (
|
||||
|
||||
@@ -210,7 +210,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
const rendeCredentials = () =>
|
||||
loginMethod["credentials"] && (
|
||||
<form onSubmit={handleSubmit2(onSubmitPwd)}>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-1">
|
||||
<Label className="sr-only" htmlFor="email">
|
||||
Email
|
||||
@@ -277,6 +277,14 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{loginMethod["credentials"] && <>{rendeCredentials()}</>}
|
||||
|
||||
{(loginMethod["google"] ||
|
||||
loginMethod["github"] ||
|
||||
loginMethod["linuxdo"]) &&
|
||||
(loginMethod["resend"] || loginMethod["credentials"]) &&
|
||||
rendeSeparator()}
|
||||
|
||||
{loginMethod["google"] && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -354,12 +362,6 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{(loginMethod["google"] ||
|
||||
loginMethod["github"] ||
|
||||
loginMethod["linuxdo"]) &&
|
||||
(loginMethod["resend"] || loginMethod["credentials"]) &&
|
||||
rendeSeparator()}
|
||||
|
||||
{/* {loginMethod["resend"] && loginMethod["credentials"] ? (
|
||||
<Tabs defaultValue="resend">
|
||||
<TabsList className="mb-2 w-full justify-center">
|
||||
@@ -375,8 +377,6 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
{rendeCredentials()}
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{loginMethod["credentials"] && <>{rendeCredentials()}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function EmailManagerInnovate() {
|
||||
const [viewMode, setViewMode] = useState("inbox"); // Toggle between inbox and sent
|
||||
|
||||
return (
|
||||
<main className="mx-auto my-8 flex w-full max-w-[561.5px] flex-col items-center justify-center rounded-2xl border border-neutral-800/[0.08] bg-gradient-to-br from-white to-blue-50/30 p-6 shadow-lg backdrop-blur-lg dark:border-neutral-700/50 dark:bg-gradient-to-br dark:from-gray-800 dark:to-black dark:shadow-xl">
|
||||
<main className="mx-auto my-8 hidden w-full max-w-[561.5px] scale-[0.8] flex-col items-center justify-center rounded-2xl border border-neutral-800/[0.08] bg-gradient-to-br from-white to-blue-50/30 p-6 shadow-lg backdrop-blur-lg dark:border-neutral-700/50 dark:bg-gradient-to-br dark:from-gray-800 dark:to-black dark:shadow-xl md:flex">
|
||||
<div className="absolute left-1/2 top-0 flex -translate-x-1/2 -translate-y-1/2 items-center gap-1.5 rounded-full border border-neutral-300 bg-[#eff9fa] px-2 py-0.5 text-xs text-neutral-600 dark:border-neutral-700/50 dark:bg-neutral-900 dark:text-neutral-300">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -10,7 +10,11 @@ import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
|
||||
import CountUpFn from "../dashboard/count-up";
|
||||
import { InfiniteSlider } from "../ui/infinite-slider";
|
||||
import { ProgressiveBlur } from "../ui/progressive-blur";
|
||||
import EmailManagerExp from "./email";
|
||||
import PreviewLanding from "./preview-landing";
|
||||
import UrlShortener from "./url-shortener";
|
||||
|
||||
export default function HeroLanding({
|
||||
@@ -20,7 +24,7 @@ export default function HeroLanding({
|
||||
}) {
|
||||
const t = useTranslations("Landing");
|
||||
return (
|
||||
<section className="custom-bg relative space-y-6 py-12 sm:py-20 lg:py-24">
|
||||
<section className="relative space-y-6 py-12 sm:py-16">
|
||||
<div className="container flex max-w-screen-lg flex-col items-center gap-5 text-center">
|
||||
<Link
|
||||
href={siteConfig.links.github}
|
||||
@@ -50,7 +54,7 @@ export default function HeroLanding({
|
||||
{t("platformDescription")}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="mb-10 flex items-center justify-center gap-4">
|
||||
{/* <GitHubStarsWithSuspense
|
||||
owner="oiov"
|
||||
repo="wr.do"
|
||||
@@ -80,7 +84,106 @@ export default function HeroLanding({
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<UrlShortener />
|
||||
<PreviewLanding />
|
||||
|
||||
<div className="group relative m-auto max-w-5xl">
|
||||
<div className="flex flex-col items-center md:flex-row">
|
||||
<div className="mb-4 hidden md:mb-0 md:block md:max-w-44 md:border-r md:border-gray-600 md:pr-6">
|
||||
<p className="text-end text-sm text-black dark:text-gray-400">
|
||||
Powering the best teams
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative py-6 md:w-[calc(100%-11rem)]">
|
||||
<InfiniteSlider durationOnHover={20} duration={40} gap={112}>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-5 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/nvidia-TAN2JNiFDeluYk9hlkv4qXwWtfx5Cy.svg"
|
||||
alt="Nvidia Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-4 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/column-qYeLfzzj1ni9E7PhooLL6Mzip5Zeb4.svg"
|
||||
alt="Column Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-4 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/github-twQNbc5nAy2jUs7yh5xic8hsEfBYpQ.svg"
|
||||
alt="GitHub Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-5 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/nike-H0OCso4JdUtllUTdAverMAjJmcKVXU.svg"
|
||||
alt="Nike Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-5 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/lemonsqueezy-ZL7mmIzqR10hWcodoO19ajha8AS9VK.svg"
|
||||
alt="Lemon Squeezy Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-4 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/laravel-sDCMR3A82V8F6ycZymrDlmiFpxyUd4.svg"
|
||||
alt="Laravel Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-7 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/lilly-Jhslk9VPUVAVK2SCJmCGTEbqKMef5v.svg"
|
||||
alt="Lilly Logo"
|
||||
height="28"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<img
|
||||
className="mx-auto h-6 w-fit opacity-80 dark:opacity-60 dark:invert"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/design-mode-images/openai-5TPubXl1hnLxeIs4ygVSLjJcUoBOCB.svg"
|
||||
alt="OpenAI Logo"
|
||||
height="24"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
</InfiniteSlider>
|
||||
|
||||
<ProgressiveBlur
|
||||
className="pointer-events-none absolute left-0 top-0 h-full w-20"
|
||||
direction="left"
|
||||
blurIntensity={1}
|
||||
/>
|
||||
<ProgressiveBlur
|
||||
className="pointer-events-none absolute right-0 top-0 h-full w-20"
|
||||
direction="right"
|
||||
blurIntensity={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -90,8 +193,17 @@ export function LandingImages() {
|
||||
const t = useTranslations("Landing");
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-10 w-full max-w-6xl px-6">
|
||||
<div className="my-14 flex flex-col items-center justify-around gap-10 md:flex-row-reverse">
|
||||
<div className="mx-auto w-full max-w-5xl px-6">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<h2 className="font-mono font-semibold uppercase tracking-wider text-blue-600">
|
||||
{t("Features")}
|
||||
</h2>
|
||||
<p className="text-balance text-2xl text-foreground">
|
||||
{"All In One Means"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-14 mt-6 flex flex-col items-center justify-around gap-10 md:flex-row-reverse">
|
||||
<Image
|
||||
className="size-[260px] rounded-lg transition-all hover:opacity-90 hover:shadow-xl"
|
||||
alt={t("exampleImageAlt")}
|
||||
@@ -227,13 +339,61 @@ export function LandingImages() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grids grids-dark mx-auto my-10 flex w-full max-w-6xl px-4">
|
||||
<DynamicData />
|
||||
|
||||
<div className="grids grids-dark mx-auto my-12 flex w-full max-w-6xl px-4">
|
||||
<UrlShortener />
|
||||
<EmailManagerExp />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DynamicData() {
|
||||
const t = useTranslations("Landing");
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto mt-10 max-w-5xl space-y-8 px-6 md:space-y-16">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<h2 className="font-mono font-semibold uppercase tracking-wider text-blue-600">
|
||||
{t("Stats")}
|
||||
</h2>
|
||||
<div className="text-balance text-2xl text-foreground">
|
||||
<span style={{ fontFamily: "Bahamas Bold" }}>WR.DO Cloud</span> in
|
||||
numbers
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-12 divide-y-0 text-center md:grid-cols-4 md:gap-2 md:divide-x">
|
||||
<div className="space-y-4">
|
||||
<div className="text-5xl font-bold text-blue-600">
|
||||
<CountUpFn count={2500} />+
|
||||
</div>
|
||||
<p>{t("Happy Customers")}</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-5xl font-bold text-blue-600">
|
||||
<CountUpFn count={6100} />+
|
||||
</div>
|
||||
<p>{t("Short Links")}</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-5xl font-bold text-blue-600">
|
||||
<CountUpFn count={19000} />+
|
||||
</div>
|
||||
<p>{t("Email Addresses")}</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-5xl font-bold text-blue-600">
|
||||
<CountUpFn count={40000} />+
|
||||
</div>
|
||||
<p>{t("Inbox Emails")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardItem({
|
||||
bgColor = "bg-yellow-400",
|
||||
rotate = "rotate-12",
|
||||
|
||||
@@ -6,7 +6,7 @@ import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
|
||||
|
||||
export default function PreviewLanding() {
|
||||
return (
|
||||
<div className="pb-6 sm:pb-20">
|
||||
<div className="pb-2 sm:pb-10">
|
||||
<MaxWidthWrapper>
|
||||
<div className="h-auto rounded-xl md:bg-muted/30 md:p-3.5 md:ring-1 md:ring-inset md:ring-border">
|
||||
<div className="relative overflow-hidden rounded-xl border md:rounded-lg">
|
||||
|
||||
@@ -12,7 +12,7 @@ import { PlanQuotaFormData } from "@/lib/dto/plan";
|
||||
import { cn, fetcher, nFormatter } from "@/lib/utils";
|
||||
|
||||
import { Icons } from "../shared/icons";
|
||||
import { Button } from "../ui/button";
|
||||
import { Button, buttonVariants } from "../ui/button";
|
||||
|
||||
const getBenefits = (plan: PlanQuotaFormData) => [
|
||||
{
|
||||
@@ -80,7 +80,7 @@ export const PricingSection = () => {
|
||||
className="relative overflow-hidden bg-zinc-50 text-zinc-800 selection:bg-zinc-200 dark:bg-zinc-950 dark:text-zinc-200 dark:selection:bg-zinc-600"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(100%_100%_at_50%_0%,rgba(245,245,245,0.8),rgba(240,240,240,1))] dark:bg-[radial-gradient(100%_100%_at_50%_0%,rgba(13,13,17,1),rgba(9,9,11,1))]"></div>
|
||||
<div className="relative z-10 mx-auto max-w-5xl px-4 py-20 md:px-8">
|
||||
<div className="relative z-10 mx-auto max-w-5xl px-4 py-20 text-center md:px-8">
|
||||
<div className="mb-12 space-y-3">
|
||||
<h2 className="text-center text-xl font-semibold leading-tight sm:text-3xl sm:leading-tight md:text-4xl md:leading-tight">
|
||||
{t("pricingTitle")}
|
||||
@@ -122,6 +122,17 @@ export const PricingSection = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/dashboard"
|
||||
prefetch={true}
|
||||
className={cn(
|
||||
buttonVariants({ rounded: "xl", size: "lg" }),
|
||||
"mx-auto mt-16 px-4 text-[15px] font-semibold",
|
||||
)}
|
||||
>
|
||||
{t("Try the cloud version")}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Icons } from "../shared/icons";
|
||||
|
||||
export default function UrlShotenerExp() {
|
||||
return (
|
||||
<main className="mx-auto mt-10 flex w-full max-w-[561.5px] flex-col items-center justify-center rounded-xl border border-neutral-900/[0.05] bg-neutral-500/5 px-4 py-5 backdrop-blur dark:border-neutral-700/50">
|
||||
<main className="mx-auto mt-10 flex w-full max-w-[561.5px] scale-[0.8] flex-col items-center justify-center rounded-xl border border-neutral-900/[0.05] bg-neutral-500/5 px-4 py-5 backdrop-blur dark:border-neutral-700/50">
|
||||
<div className="absolute left-1/2 top-0 flex -translate-x-1/2 -translate-y-1/2 items-center gap-1.5 rounded-full border border-neutral-300 bg-[#eff9fa] px-2 py-0.5 text-xs text-neutral-600 dark:border-neutral-700/50 dark:bg-neutral-900 dark:text-neutral-300">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
103
components/shared/flip-words.tsx
Normal file
103
components/shared/flip-words.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMounted } from "@/hooks/use-mounted";
|
||||
|
||||
export const FlipWords = ({
|
||||
words,
|
||||
duration = 3000,
|
||||
className,
|
||||
}: {
|
||||
words: string[];
|
||||
duration?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [currentWord, setCurrentWord] = useState(words[0]);
|
||||
const [isAnimating, setIsAnimating] = useState<boolean>(false);
|
||||
const mounted = useMounted();
|
||||
|
||||
// thanks for the fix Julian - https://github.com/Julian-AT
|
||||
const startAnimation = useCallback(() => {
|
||||
const word = words[words.indexOf(currentWord) + 1] || words[0];
|
||||
setCurrentWord(word);
|
||||
setIsAnimating(true);
|
||||
}, [currentWord, words]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnimating)
|
||||
setTimeout(() => {
|
||||
startAnimation();
|
||||
}, duration);
|
||||
}, [isAnimating, duration, startAnimation]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence
|
||||
onExitComplete={() => {
|
||||
setIsAnimating(false);
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -40,
|
||||
x: 40,
|
||||
filter: "blur(8px)",
|
||||
scale: 2,
|
||||
position: "absolute",
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-10 inline-block px-2 text-left text-foreground",
|
||||
className,
|
||||
)}
|
||||
key={currentWord}
|
||||
>
|
||||
{currentWord.split(" ").map((word, wordIndex) => (
|
||||
<motion.span
|
||||
key={word + wordIndex}
|
||||
initial={{ opacity: 0, y: 10, filter: "blur(4px)" }}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{
|
||||
delay: wordIndex * 0.01,
|
||||
duration: 0.03,
|
||||
}}
|
||||
className="inline-block whitespace-nowrap"
|
||||
>
|
||||
{word.split("").map((letter, letterIndex) => (
|
||||
<motion.span
|
||||
key={word + letterIndex}
|
||||
initial={{ opacity: 0, y: 10, filter: "blur(4px)" }}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{
|
||||
delay: wordIndex * 0.2 + letterIndex * 0.08,
|
||||
duration: 0.2,
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
{letter}
|
||||
</motion.span>
|
||||
))}
|
||||
<span className="inline-block"> </span>
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
61
components/ui/background-paths.tsx
Normal file
61
components/ui/background-paths.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
function FloatingPaths({ position }: { position: number }) {
|
||||
const paths = Array.from({ length: 36 }, (_, i) => ({
|
||||
id: i,
|
||||
d: `M-${380 - i * 5 * position} -${189 + i * 6}C-${
|
||||
380 - i * 5 * position
|
||||
} -${189 + i * 6} -${312 - i * 5 * position} ${216 - i * 6} ${
|
||||
152 - i * 5 * position
|
||||
} ${343 - i * 6}C${616 - i * 5 * position} ${470 - i * 6} ${
|
||||
684 - i * 5 * position
|
||||
} ${875 - i * 6} ${684 - i * 5 * position} ${875 - i * 6}`,
|
||||
color: `rgba(15,23,42,${0.1 + i * 0.03})`,
|
||||
width: 0.5 + i * 0.03,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<svg
|
||||
className="w-full h-full text-muted-foreground"
|
||||
viewBox="0 0 696 316"
|
||||
fill="none"
|
||||
>
|
||||
<title>Background Paths</title>
|
||||
{paths.map((path) => (
|
||||
<motion.path
|
||||
key={path.id}
|
||||
d={path.d}
|
||||
stroke="currentColor"
|
||||
strokeWidth={path.width}
|
||||
strokeOpacity={0.1 + path.id * 0.03}
|
||||
initial={{ pathLength: 0.3, opacity: 0.6 }}
|
||||
animate={{
|
||||
pathLength: 1,
|
||||
opacity: [0.3, 0.6, 0.3],
|
||||
pathOffset: [0, 1, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 20 + Math.random() * 10,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BackgroundPaths() {
|
||||
return (
|
||||
<div className="relative h-full w-full flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<FloatingPaths position={1} />
|
||||
<FloatingPaths position={-1} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
96
components/ui/infinite-slider.tsx
Normal file
96
components/ui/infinite-slider.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type React from "react"
|
||||
|
||||
import { useMotionValue, animate, motion } from "framer-motion"
|
||||
import { useState, useEffect } from "react"
|
||||
import useMeasure from "react-use-measure"
|
||||
|
||||
type InfiniteSliderProps = {
|
||||
children: React.ReactNode
|
||||
gap?: number
|
||||
duration?: number
|
||||
durationOnHover?: number
|
||||
direction?: "horizontal" | "vertical"
|
||||
reverse?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function InfiniteSlider({
|
||||
children,
|
||||
gap = 16,
|
||||
duration = 25,
|
||||
durationOnHover,
|
||||
direction = "horizontal",
|
||||
reverse = false,
|
||||
className,
|
||||
}: InfiniteSliderProps) {
|
||||
const [currentDuration, setCurrentDuration] = useState(duration)
|
||||
const [ref, { width, height }] = useMeasure()
|
||||
const translation = useMotionValue(0)
|
||||
const [isTransitioning, setIsTransitioning] = useState(false)
|
||||
const [key, setKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
let controls
|
||||
const size = direction === "horizontal" ? width : height
|
||||
const contentSize = size + gap
|
||||
const from = reverse ? -contentSize / 2 : 0
|
||||
const to = reverse ? 0 : -contentSize / 2
|
||||
|
||||
if (isTransitioning) {
|
||||
controls = animate(translation, [translation.get(), to], {
|
||||
ease: "linear",
|
||||
duration: currentDuration * Math.abs((translation.get() - to) / contentSize),
|
||||
onComplete: () => {
|
||||
setIsTransitioning(false)
|
||||
setKey((prevKey) => prevKey + 1)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
controls = animate(translation, [from, to], {
|
||||
ease: "linear",
|
||||
duration: currentDuration,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
repeatType: "loop",
|
||||
repeatDelay: 0,
|
||||
onRepeat: () => {
|
||||
translation.set(from)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return controls?.stop
|
||||
}, [key, translation, currentDuration, width, height, gap, isTransitioning, direction, reverse])
|
||||
|
||||
const hoverProps = durationOnHover
|
||||
? {
|
||||
onHoverStart: () => {
|
||||
setIsTransitioning(true)
|
||||
setCurrentDuration(durationOnHover)
|
||||
},
|
||||
onHoverEnd: () => {
|
||||
setIsTransitioning(true)
|
||||
setCurrentDuration(duration)
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden", className)}>
|
||||
<motion.div
|
||||
className="flex w-max"
|
||||
style={{
|
||||
...(direction === "horizontal" ? { x: translation } : { y: translation }),
|
||||
gap: `${gap}px`,
|
||||
flexDirection: direction === "horizontal" ? "row" : "column",
|
||||
}}
|
||||
ref={ref}
|
||||
{...hoverProps}
|
||||
>
|
||||
{children}
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
components/ui/progressive-blur.tsx
Normal file
64
components/ui/progressive-blur.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLMotionProps, motion } from "framer-motion";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// import { type HTMLMotionProps, motion } from "motion/react"
|
||||
|
||||
export const GRADIENT_ANGLES = {
|
||||
top: 0,
|
||||
right: 90,
|
||||
bottom: 180,
|
||||
left: 270,
|
||||
};
|
||||
|
||||
export type ProgressiveBlurProps = {
|
||||
direction?: keyof typeof GRADIENT_ANGLES;
|
||||
blurLayers?: number;
|
||||
className?: string;
|
||||
blurIntensity?: number;
|
||||
} & HTMLMotionProps<"div">;
|
||||
|
||||
export function ProgressiveBlur({
|
||||
direction = "bottom",
|
||||
blurLayers = 8,
|
||||
className,
|
||||
blurIntensity = 0.25,
|
||||
...props
|
||||
}: ProgressiveBlurProps) {
|
||||
const layers = Math.max(blurLayers, 2);
|
||||
const segmentSize = 1 / (blurLayers + 1);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
{Array.from({ length: layers }).map((_, index) => {
|
||||
const angle = GRADIENT_ANGLES[direction];
|
||||
const gradientStops = [
|
||||
index * segmentSize,
|
||||
(index + 1) * segmentSize,
|
||||
(index + 2) * segmentSize,
|
||||
(index + 3) * segmentSize,
|
||||
].map(
|
||||
(pos, posIndex) =>
|
||||
`rgba(255, 255, 255, ${posIndex === 1 || posIndex === 2 ? 1 : 0}) ${pos * 100}%`,
|
||||
);
|
||||
|
||||
const gradient = `linear-gradient(${angle}deg, ${gradientStops.join(", ")})`;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="pointer-events-none absolute inset-0 rounded-[inherit]"
|
||||
style={{
|
||||
maskImage: gradient,
|
||||
WebkitMaskImage: gradient,
|
||||
backdropFilter: `blur(${index * blurIntensity}px)`,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
components/ui/think.tsx
Normal file
21
components/ui/think.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export const Think = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="h-2 w-2 rounded-full bg-primary"
|
||||
animate={{
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [0.6, 1, 0.6],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -437,11 +437,19 @@
|
||||
"enterpriseTier": "Enterprise",
|
||||
"enterprisePrice": "Contact us",
|
||||
"enterpriseBestFor": "For large organizations with custom needs",
|
||||
"contactUs": "Contact us"
|
||||
"contactUs": "Contact us",
|
||||
"Try the cloud version": "Try the cloud version",
|
||||
"Features": "Features",
|
||||
"All in one means": "All in one means",
|
||||
"Stats": "Stats",
|
||||
"Happy Customers": "Happy Customers",
|
||||
"Short Links": "Short Links",
|
||||
"Email Addresses": "Email Addresses",
|
||||
"Inbox Emails": "Inbox Emails"
|
||||
},
|
||||
"Auth": {
|
||||
"Back": "Back",
|
||||
"Welcome to": "Welcome to",
|
||||
"Welcome to": "Welcome back",
|
||||
"Choose your login method to continue": "Choose your login method to continue",
|
||||
"By clicking continue, you agree to our": "By clicking continue, you agree to our",
|
||||
"Terms of Service": "Terms of Service",
|
||||
@@ -464,7 +472,8 @@
|
||||
"Administrator has disabled new user registration": "Administrator has disabled new user registration",
|
||||
"Email domain not supported, Please use one of the following:": "Email domain not supported. Please use one of the following:",
|
||||
"Auth configuration error": "Auth configuration error",
|
||||
"Unknown error": "Unknown error"
|
||||
"Unknown error": "Unknown error",
|
||||
"description": "Welcome to WR.DO, your all-in-one domain services platform. Sign in to experience our cloud tools."
|
||||
},
|
||||
"System": {
|
||||
"MENU": "Menu",
|
||||
|
||||
@@ -437,17 +437,25 @@
|
||||
"enterpriseTier": "企业计划",
|
||||
"enterprisePrice": "联系我们",
|
||||
"enterpriseBestFor": "适合有定制需求的大型组织",
|
||||
"contactUs": "联系我们"
|
||||
"contactUs": "联系我们",
|
||||
"Try the cloud version": "立即登录体验云版本",
|
||||
"Features": "功能特性",
|
||||
"All in one means": "All in one means",
|
||||
"Stats": "数据统计",
|
||||
"Happy Customers": "累计注册用户",
|
||||
"Short Links": "创建短链数量",
|
||||
"Email Addresses": "用户邮箱数量",
|
||||
"Inbox Emails": "接收邮件数量"
|
||||
},
|
||||
"Auth": {
|
||||
"Back": "返回",
|
||||
"Welcome to": "欢迎使用",
|
||||
"Choose your login method to continue": "选择登录方式以继续",
|
||||
"Welcome to": "欢迎回来",
|
||||
"Choose your login method to continue": "登录您的账户以继续",
|
||||
"By clicking continue, you agree to our": "点击继续即表示您同意我们的",
|
||||
"Terms of Service": "服务条款",
|
||||
"and": "和",
|
||||
"Privacy Policy": "隐私政策",
|
||||
"Or continue with": "或",
|
||||
"Or continue with": "或继续使用",
|
||||
"Email": "邮箱",
|
||||
"Sign Up with Email": "使用邮箱注册",
|
||||
"Sign In with Email": "使用邮箱登录",
|
||||
@@ -464,7 +472,8 @@
|
||||
"Administrator has disabled new user registration": "管理员已关闭新用户注册",
|
||||
"Email domain not supported, Please use one of the following:": "暂不支持此邮箱后缀,请使用以下邮箱:",
|
||||
"Auth configuration error": "权限校验配置错误",
|
||||
"Unknown error": "未知错误"
|
||||
"Unknown error": "未知错误",
|
||||
"description": "欢迎来到 WR.DO 一站式域名服务平台。登录以体验我们的云工具。"
|
||||
},
|
||||
"System": {
|
||||
"MENU": "菜单",
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"recharts": "^2.12.7",
|
||||
"resend": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -296,6 +296,9 @@ importers:
|
||||
react-textarea-autosize:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3(@types/react@18.3.3)(react@18.3.1)
|
||||
react-use-measure:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
recharts:
|
||||
specifier: ^2.12.7
|
||||
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -7503,6 +7506,15 @@ packages:
|
||||
react: '>=16.6.0'
|
||||
react-dom: '>=16.6.0'
|
||||
|
||||
react-use-measure@2.1.7:
|
||||
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
|
||||
peerDependencies:
|
||||
react: '>=16.13'
|
||||
react-dom: '>=16.13'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
react@18.3.1:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -17773,6 +17785,12 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react@18.3.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user