refact: better landing and login page

This commit is contained in:
oiov
2025-10-16 20:37:26 +08:00
parent 940b02313c
commit 70857c7fec
19 changed files with 627 additions and 37 deletions

View File

@@ -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>
);
}

View File

@@ -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")}

View File

@@ -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 (

View File

@@ -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>
);
}

View File

@@ -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"

View File

@@ -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",

View File

@@ -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">

View File

@@ -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>
);

View File

@@ -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"

View 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">&nbsp;</span>
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};

View 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>
);
}

View 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>
)
}

View 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
View 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,
}}
/>
);
};

View File

@@ -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",

View File

@@ -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": "菜单",

View File

@@ -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
View File

@@ -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