@windrun-huaiin/third-ui 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/clerk/index.d.mts +33 -0
  4. package/dist/clerk/index.d.ts +33 -0
  5. package/dist/clerk/index.js +2395 -0
  6. package/dist/clerk/index.js.map +1 -0
  7. package/dist/clerk/index.mjs +2361 -0
  8. package/dist/clerk/index.mjs.map +1 -0
  9. package/dist/cta.css +16 -0
  10. package/dist/fuma/index.d.mts +51 -0
  11. package/dist/fuma/index.d.ts +51 -0
  12. package/dist/fuma/index.js +2976 -0
  13. package/dist/fuma/index.js.map +1 -0
  14. package/dist/fuma/index.mjs +2944 -0
  15. package/dist/fuma/index.mjs.map +1 -0
  16. package/dist/fuma/mdx/index.d.mts +94 -0
  17. package/dist/fuma/mdx/index.d.ts +94 -0
  18. package/dist/fuma/mdx/index.js +2866 -0
  19. package/dist/fuma/mdx/index.js.map +1 -0
  20. package/dist/fuma/mdx/index.mjs +2827 -0
  21. package/dist/fuma/mdx/index.mjs.map +1 -0
  22. package/dist/fuma.css +132 -0
  23. package/dist/index.d.mts +5 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.js +3597 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/index.mjs +3549 -0
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/lib/index.d.mts +4414 -0
  30. package/dist/lib/index.d.ts +4414 -0
  31. package/dist/lib/index.js +127 -0
  32. package/dist/lib/index.js.map +1 -0
  33. package/dist/lib/index.mjs +95 -0
  34. package/dist/lib/index.mjs.map +1 -0
  35. package/dist/main/index.d.mts +25 -0
  36. package/dist/main/index.d.ts +25 -0
  37. package/dist/main/index.js +3003 -0
  38. package/dist/main/index.js.map +1 -0
  39. package/dist/main/index.mjs +2963 -0
  40. package/dist/main/index.mjs.map +1 -0
  41. package/dist/third-ui.css +44 -0
  42. package/package.json +106 -0
  43. package/src/clerk/clerk-organization.tsx +47 -0
  44. package/src/clerk/clerk-page-generator.tsx +42 -0
  45. package/src/clerk/clerk-provider-client.tsx +57 -0
  46. package/src/clerk/clerk-user.tsx +59 -0
  47. package/src/clerk/index.ts +5 -0
  48. package/src/fuma/fuma-banner-suit.tsx +16 -0
  49. package/src/fuma/fuma-github-info.tsx +194 -0
  50. package/src/fuma/fuma-page-genarator.tsx +94 -0
  51. package/src/fuma/index.ts +4 -0
  52. package/src/fuma/mdx/airtical-card.tsx +56 -0
  53. package/src/fuma/mdx/gradient-button.tsx +62 -0
  54. package/src/fuma/mdx/image-grid.tsx +35 -0
  55. package/src/fuma/mdx/image-zoom.tsx +84 -0
  56. package/src/fuma/mdx/index.ts +8 -0
  57. package/src/fuma/mdx/mermaid.tsx +87 -0
  58. package/src/fuma/mdx/toc-base.tsx +88 -0
  59. package/src/fuma/mdx/toc.tsx +35 -0
  60. package/src/fuma/mdx/trophy-card.tsx +36 -0
  61. package/src/fuma/mdx/zia-card.tsx +46 -0
  62. package/src/index.ts +4 -0
  63. package/src/lib/clerk-intl.ts +13 -0
  64. package/src/lib/fuma-schema-check-util.ts +73 -0
  65. package/src/lib/fuma-search-util.ts +6 -0
  66. package/src/lib/index.ts +3 -0
  67. package/src/main/ads-alert-dialog.tsx +133 -0
  68. package/src/main/cta.tsx +28 -0
  69. package/src/main/faq.tsx +58 -0
  70. package/src/main/features.tsx +35 -0
  71. package/src/main/footer.tsx +37 -0
  72. package/src/main/gallery.tsx +68 -0
  73. package/src/main/go-to-top.tsx +44 -0
  74. package/src/main/index.ts +12 -0
  75. package/src/main/loading.tsx +93 -0
  76. package/src/main/nprogress-bar.tsx +24 -0
  77. package/src/main/seo-content.tsx +34 -0
  78. package/src/main/tips.tsx +38 -0
  79. package/src/main/usage.tsx +45 -0
  80. package/src/styles/cta.css +16 -0
  81. package/src/styles/fuma.css +132 -0
  82. package/src/styles/third-ui.css +43 -0
@@ -0,0 +1,133 @@
1
+ import React, { useState } from "react";
2
+ import Image from "next/image";
3
+ import { globalLucideIcons as icons } from "@base-ui/components/global-icon";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogContent,
7
+ AlertDialogTitle,
8
+ AlertDialogDescription,
9
+ AlertDialogAction,
10
+ } from "@base-ui/ui/alert-dialog";
11
+
12
+ interface AdsAlertDialogProps {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ title: React.ReactNode;
16
+ description: React.ReactNode;
17
+ imgSrc?: string;
18
+ imgHref?: string;
19
+ onCancel?: () => void;
20
+ cancelText?: string;
21
+ confirmText?: string;
22
+ onConfirm?: () => void;
23
+ }
24
+
25
+ export function AdsAlertDialog({
26
+ open,
27
+ onOpenChange,
28
+ title,
29
+ description,
30
+ imgSrc,
31
+ imgHref,
32
+ cancelText,
33
+ onCancel,
34
+ confirmText,
35
+ onConfirm,
36
+ }: AdsAlertDialogProps) {
37
+ const [imgError, setImgError] = useState(false);
38
+
39
+ return (
40
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
41
+ <AlertDialogContent
42
+ className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-200 dark:border-neutral-700 max-w-md w-full min-w-[320px] p-4 flex flex-col items-stretch"
43
+ >
44
+ {/* Header: left icon + title, right X close */}
45
+ <div className="flex flex-row items-center justify-between mb-2">
46
+ <AlertDialogTitle asChild>
47
+ <div className="flex flex-row items-center gap-1 min-w-0 text-xl font-semibold">
48
+ <icons.Info className="w-5 h-5" />
49
+ <span className="truncate">{title}</span>
50
+ </div>
51
+ </AlertDialogTitle>
52
+ <button
53
+ className="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 text-xl ml-4"
54
+ onClick={() => onOpenChange(false)}
55
+ aria-label="Close"
56
+ tabIndex={0}
57
+ >
58
+ <icons.X className="w-5 h-5" />
59
+ </button>
60
+ </div>
61
+
62
+ {/* description area */}
63
+ <AlertDialogDescription className="text-base font-medium text-neutral-800 dark:text-neutral-100 mb-2">
64
+ {description}
65
+ </AlertDialogDescription>
66
+ {/* image area (optional) */}
67
+ {imgSrc && (
68
+ <div className="w-full max-w-[400px] h-[220px] relative flex items-center justify-center mb-2">
69
+ {imgError ? (
70
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-100 dark:bg-neutral-800 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-lg text-neutral-400 text-sm">
71
+ <icons.ImageOff className="w-12 h-12 mb-2" />
72
+ <span>Image loading failed</span>
73
+ </div>
74
+ ) : imgHref ? (
75
+ <a href={imgHref} target="_blank" rel="noopener noreferrer" className="block w-full h-full">
76
+ <Image
77
+ src={imgSrc}
78
+ alt="image"
79
+ fill
80
+ className="object-contain rounded-lg"
81
+ priority={false}
82
+ placeholder="empty"
83
+ unoptimized
84
+ onError={() => setImgError(true)}
85
+ sizes="(max-width: 400px) 100vw, 400px"
86
+ />
87
+ </a>
88
+ ) : (
89
+ <Image
90
+ src={imgSrc}
91
+ alt="image"
92
+ fill
93
+ className="object-contain rounded-lg"
94
+ priority={false}
95
+ placeholder="empty"
96
+ unoptimized
97
+ onError={() => setImgError(true)}
98
+ sizes="(max-width: 400px) 100vw, 400px"
99
+ />
100
+ )}
101
+ </div>
102
+ )}
103
+ {/* button area (optional) */}
104
+ {(cancelText || confirmText) && (
105
+ <div className="flex justify-end gap-2 mt-2">
106
+ {cancelText && (
107
+ <button
108
+ onClick={() => {
109
+ onOpenChange(false);
110
+ onCancel?.();
111
+ }}
112
+ className="px-6 py-2 rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-200 font-semibold hover:bg-neutral-100 dark:hover:bg-neutral-700 transition"
113
+ >
114
+ {cancelText}
115
+ </button>
116
+ )}
117
+ {confirmText && (
118
+ <AlertDialogAction
119
+ onClick={() => {
120
+ onOpenChange(false);
121
+ onConfirm?.();
122
+ }}
123
+ className="px-6 py-2 rounded-lg bg-purple-500 text-white font-semibold hover:bg-purple-600 transition"
124
+ >
125
+ {confirmText}
126
+ </AlertDialogAction>
127
+ )}
128
+ </div>
129
+ )}
130
+ </AlertDialogContent>
131
+ </AlertDialog>
132
+ );
133
+ }
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ import { GradientButton } from "@third-ui/fuma/mdx/gradient-button";
4
+ import { useTranslations } from 'next-intl';
5
+
6
+ export function CTA() {
7
+ const t = useTranslations('cta');
8
+ return (
9
+ <section className="container mx-auto px-4 py-20">
10
+ <div className="bg-gradient-to-r from-purple-200 to-pink-300 rounded-3xl p-12 text-center text-white bg-[length:200%_auto] animate-cta-gradient-wave">
11
+ <h2 className="text-3xl md:text-4xl font-bold mb-6">
12
+ {t('title')} <span className="text-purple-400">{t('eyesOn')}</span>?
13
+ </h2>
14
+ <p className="text-xl max-w-2xl mx-auto mb-8 text-white/80">
15
+ {t('description1')}
16
+ <br />
17
+ {t('description2')}
18
+ </p>
19
+ <GradientButton
20
+ title={t('button')}
21
+ href="https://preview.reve.art/"
22
+ align="center"
23
+ />
24
+ </div>
25
+ </section>
26
+ )
27
+ }
28
+
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { useTranslations } from 'next-intl';
4
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
5
+
6
+ export function FAQ() {
7
+ const t = useTranslations('faq');
8
+ const items = t.raw('items') as Array<{
9
+ question: string;
10
+ answer: string;
11
+ }>;
12
+ const [openArr, setOpenArr] = useState<boolean[]>(() => items.map(() => false));
13
+
14
+ const handleToggle = (idx: number) => {
15
+ setOpenArr(prev => {
16
+ const next = [...prev];
17
+ next[idx] = !next[idx];
18
+ return next;
19
+ });
20
+ };
21
+
22
+ return (
23
+ <section id="faq" className="px-16 py-10 mx-16 md:mx-32">
24
+ <h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
25
+ {t('title')}
26
+ </h2>
27
+ <p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto">
28
+ {t('description')}
29
+ </p>
30
+ <div className="space-y-6">
31
+ {items.map((item, idx) => {
32
+ const isOpen = openArr[idx];
33
+ const Icon = isOpen ? icons.ChevronDown : icons.ChevronRight;
34
+ return (
35
+ <div
36
+ key={idx}
37
+ className="bg-white dark:bg-gray-800/60 p-6 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-500/50 transition shadow-sm dark:shadow-none"
38
+ >
39
+ <button
40
+ className="w-full flex items-center justify-between text-left focus:outline-none"
41
+ onClick={() => handleToggle(idx)}
42
+ aria-expanded={isOpen}
43
+ >
44
+ <span className="text-lg font-semibold text-gray-900 dark:text-gray-100">{item.question}</span>
45
+ <Icon className="w-6 h-6 text-gray-400 ml-2 transition-transform duration-200" />
46
+ </button>
47
+ {isOpen && (
48
+ <div className="mt-4 text-gray-700 dark:text-gray-300 text-base">
49
+ {item.answer}
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ })}
55
+ </div>
56
+ </section>
57
+ );
58
+ }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import { useTranslations } from 'next-intl'
4
+
5
+ export function Features() {
6
+ const t = useTranslations('features');
7
+
8
+ // 直接从翻译文件获取特性列表
9
+ const featureItems = t.raw('items') as Array<{
10
+ title: string;
11
+ description: string;
12
+ icon: string;
13
+ }>;
14
+
15
+ return (
16
+ <section id="features" className="container mx-auto px-4 py-20">
17
+ <h2 className="text-3xl md:text-4xl font-bold text-center mb-16">
18
+ {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
19
+ </h2>
20
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
21
+ {featureItems.map((feature, index) => (
22
+ <div
23
+ key={index}
24
+ className="bg-gray-50 dark:bg-gray-800/50 p-8 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-500/50 transition"
25
+ >
26
+ <div className="text-4xl mb-4">{feature.icon}</div>
27
+ <h3 className="text-xl font-semibold mb-3">{feature.title}</h3>
28
+ <p className="">{feature.description}</p>
29
+ </div>
30
+ ))}
31
+ </div>
32
+ </section>
33
+ )
34
+ }
35
+
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
4
+ import { useLocale, useTranslations } from 'next-intl';
5
+ import Link from "next/link";
6
+
7
+ export function Footer() {
8
+ const tFooter = useTranslations('footer');
9
+ const locale = useLocale();
10
+
11
+ return (
12
+ <div className="mb-10 w-full mx-auto border-t-purple-700/80 border-t-1">
13
+ <footer>
14
+ <div className="w-full flex flex-col items-center justify-center px-4 py-8 space-y-3">
15
+ {/* 第一行:居中icon跳转链接 */}
16
+ <div className="flex items-center justify-center space-x-6 text-xs">
17
+ <Link href={`/${locale}/legal/terms`} className="flex items-center space-x-1 hover:underline">
18
+ <icons.ReceiptText className="h-3.5 w-3.5"/>
19
+ <span>{tFooter('terms', { defaultValue: 'Terms of Service' })}</span>
20
+ </Link>
21
+ <Link href={`/${locale}/legal/privacy`} className="flex items-center space-x-1 hover:underline">
22
+ <icons.ShieldUser className="h-3.5 w-3.5"/>
23
+ <span>{tFooter('privacy', { defaultValue: 'Privacy Policy' })}</span>
24
+ </Link>
25
+ </div>
26
+ {/* 第二行:版权声明 */}
27
+ <div className="text-xs text-center">
28
+ <span>
29
+ {tFooter('copyright', { year: new Date().getFullYear(), name: tFooter('company') })}
30
+ </span>
31
+ </div>
32
+ </div>
33
+ </footer>
34
+ </div>
35
+ );
36
+ }
37
+
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
4
+ import { useTranslations } from 'next-intl'
5
+ import Image from "next/image"
6
+ import { GradientButton } from "@third-ui/fuma/mdx/gradient-button"
7
+
8
+ export function Gallery() {
9
+ const t = useTranslations('gallery');
10
+ const galleryItems = t.raw('prompts') as string[];
11
+
12
+ const handleDownload = async (index: number) => {
13
+ try {
14
+ const response = await fetch(`/${index + 1}.webp`);
15
+ const blob = await response.blob();
16
+ const url = window.URL.createObjectURL(blob);
17
+ const a = document.createElement('a');
18
+ a.href = url;
19
+ a.download = `reve-image-${index + 1}.webp`;
20
+ document.body.appendChild(a);
21
+ a.click();
22
+ window.URL.revokeObjectURL(url);
23
+ document.body.removeChild(a);
24
+ } catch (error) {
25
+ console.error('Download failed:', error);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <section id="gallery" className="container mx-auto px-4 py-20">
31
+ <h2 className="text-3xl md:text-4xl font-bold text-center mb-6">
32
+ {t('titleL')} <span className="text-purple-500">{t('eyesOn')}</span> {t('titleR')}
33
+ </h2>
34
+ <p className="text-center max-w-2xl mx-auto mb-16">
35
+ {t('description')}
36
+ </p>
37
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
38
+ {galleryItems.map((prompt, index) => (
39
+ <div key={index} className="group relative overflow-hidden rounded-xl">
40
+ <Image
41
+ src={`/${index + 1}.webp`}
42
+ alt="Reve Image AI-generated artwork"
43
+ width={600}
44
+ height={600}
45
+ className="w-full h-80 object-cover transition duration-300 group-hover:scale-105"
46
+ />
47
+ <div className="absolute inset-0 flex items-end justify-end p-4 opacity-0 group-hover:opacity-100 transition duration-300">
48
+ <button
49
+ onClick={() => handleDownload(index)}
50
+ className="bg-black/50 hover:bg-black/70 p-2 rounded-full text-white/80 hover:text-white transition-all duration-300"
51
+ >
52
+ <icons.Download className="h-5 w-5 text-white" />
53
+ </button>
54
+ </div>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ <div className="text-center mt-12">
59
+ <GradientButton
60
+ title={t('button')}
61
+ href="https://preview.reve.art/"
62
+ align="center"
63
+ />
64
+ </div>
65
+ </section>
66
+ )
67
+ }
68
+
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
5
+
6
+ export function GoToTop() {
7
+ const [isVisible, setIsVisible] = useState(false);
8
+
9
+ // listen to scroll event
10
+ useEffect(() => {
11
+ const toggleVisibility = () => {
12
+ if (window.scrollY > 300) {
13
+ setIsVisible(true);
14
+ } else {
15
+ setIsVisible(false);
16
+ }
17
+ };
18
+
19
+ window.addEventListener('scroll', toggleVisibility);
20
+ return () => window.removeEventListener('scroll', toggleVisibility);
21
+ }, []);
22
+
23
+ // scroll to top
24
+ const scrollToTop = () => {
25
+ window.scrollTo({
26
+ top: 0,
27
+ behavior: 'smooth'
28
+ });
29
+ };
30
+
31
+ return (
32
+ <>
33
+ {isVisible && (
34
+ <button
35
+ onClick={scrollToTop}
36
+ className="fixed bottom-6 right-6 p-3 bg-neutral-800 text-neutral-100 hover:bg-neutral-700 dark:bg-neutral-300 dark:text-neutral-900 dark:hover:bg-neutral-400 rounded-full shadow-lg transition-all z-50"
37
+ aria-label="Go to top"
38
+ >
39
+ <icons.ArrowUp size={20} />
40
+ </button>
41
+ )}
42
+ </>
43
+ );
44
+ }
@@ -0,0 +1,12 @@
1
+ // Main application components
2
+ export * from './gallery';
3
+ export * from './usage';
4
+ export * from './features';
5
+ export * from './tips';
6
+ export * from './faq';
7
+ export * from './seo-content';
8
+ export * from './cta';
9
+ export * from './footer';
10
+ export * from './go-to-top';
11
+ export * from './loading';
12
+ export * from './nprogress-bar';
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ const NUM_ROWS = 15;
4
+ const NUM_COLS = 15;
5
+ const DOT_SIZE = 6; // px, dot diameter
6
+ const SPACING = 12; // px, space between dot centers
7
+ const ANIMATION_DURATION = 1.8; // seconds
8
+ const STAGGER_DELAY_FACTOR = 0.08; // seconds, delay per unit of distance from center
9
+
10
+ // Expanded palette for more "dazzling" effect
11
+ const COLORS = [
12
+ '#AC62FD', // Base Purple
13
+ '#C364FA',
14
+ '#DD59F7',
15
+ '#FF4FF2', // Bright Pink/Magenta
16
+ '#F067DD',
17
+ '#E26AF8',
18
+ '#DA70D6', // Orchid
19
+ '#BA55D3', // Medium Orchid
20
+ '#9370DB', // Medium Purple
21
+ '#8A2BE2' // Blue Violet
22
+ ];
23
+
24
+ export function Loading() {
25
+ const dots = [];
26
+ const centerX = (NUM_COLS - 1) / 2;
27
+ const centerY = (NUM_ROWS - 1) / 2;
28
+
29
+ for (let i = 0; i < NUM_ROWS; i++) {
30
+ for (let j = 0; j < NUM_COLS; j++) {
31
+ // Calculate distance from the center of the grid
32
+ const distance = Math.sqrt(Math.pow(i - centerY, 2) + Math.pow(j - centerX, 2));
33
+ dots.push({
34
+ id: `${i}-${j}`,
35
+ row: i,
36
+ col: j,
37
+ // Animation delay based on distance, creating a ripple effect
38
+ delay: distance * STAGGER_DELAY_FACTOR,
39
+ // Color selection based on distance rings
40
+ color: COLORS[Math.floor(distance) % COLORS.length],
41
+ });
42
+ }
43
+ }
44
+
45
+ // Calculate the total width and height of the dot container
46
+ const containerWidth = (NUM_COLS - 1) * SPACING + DOT_SIZE;
47
+ const containerHeight = (NUM_ROWS - 1) * SPACING + DOT_SIZE;
48
+
49
+ return (
50
+ <div className="flex flex-col items-center justify-center min-h-screen bg-neutral-900">
51
+ <div
52
+ style={{
53
+ width: containerWidth,
54
+ height: containerHeight,
55
+ position: 'relative',
56
+ borderRadius: '50%', // Make the container circular
57
+ overflow: 'hidden', // Clip dots outside the circle
58
+ }}
59
+ >
60
+ {dots.map(dot => (
61
+ <div
62
+ key={dot.id}
63
+ style={{
64
+ position: 'absolute',
65
+ left: dot.col * SPACING,
66
+ top: dot.row * SPACING,
67
+ width: DOT_SIZE,
68
+ height: DOT_SIZE,
69
+ backgroundColor: dot.color,
70
+ borderRadius: '50%',
71
+ animationName: 'loading-dot-pulse',
72
+ animationDuration: `${ANIMATION_DURATION}s`,
73
+ animationTimingFunction: 'cubic-bezier(0.45, 0, 0.55, 1)',
74
+ animationIterationCount: 'infinite',
75
+ animationDelay: `${dot.delay}s`,
76
+ opacity: 0,
77
+ transform: 'scale(0)',
78
+ }}
79
+ />
80
+ ))}
81
+ {/* Centered Loading Text */}
82
+ <div
83
+ className="absolute inset-0 flex items-center justify-center"
84
+ style={{ pointerEvents: 'none' }} // So text doesn't interfere with potential mouse events on dots if any
85
+ >
86
+ <p className="text-xl font-semibold text-white">
87
+ Loading...
88
+ </p>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+ import NProgress from 'nprogress'
3
+ import { usePathname } from 'next/navigation'
4
+ import { useEffect, useRef } from 'react'
5
+
6
+ // remove NProgress progress bar spinner circle
7
+ NProgress.configure({ showSpinner: false })
8
+
9
+ export function NProgressBar() {
10
+ const pathname = usePathname()
11
+ const previousPath = useRef(pathname)
12
+
13
+ useEffect(() => {
14
+ if (previousPath.current !== pathname) {
15
+ NProgress.start()
16
+ setTimeout(() => {
17
+ NProgress.done()
18
+ }, 100)
19
+ previousPath.current = pathname
20
+ }
21
+ }, [pathname])
22
+
23
+ return null
24
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable react/no-unescaped-entities */
2
+ 'use client'
3
+
4
+ import { useTranslations } from 'next-intl'
5
+
6
+ interface Section {
7
+ title: string;
8
+ content: string;
9
+ }
10
+
11
+ export function SeoContent() {
12
+ const t = useTranslations('seoContent');
13
+
14
+ return (
15
+ <section className="container mx-auto px-4 py-20">
16
+ <h2 className="text-3xl md:text-4xl font-bold text-center mb-16">
17
+ {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
18
+ </h2>
19
+ <div className="prose prose-lg dark:prose-invert max-w-4xl mx-auto">
20
+ <p>{t('intro')}</p>
21
+
22
+ {t.raw('sections').map((section: Section, index: number) => (
23
+ <div key={index}>
24
+ <h3>{t(`sections.${index}.title`)}</h3>
25
+ <p>{t(`sections.${index}.content`)}</p>
26
+ </div>
27
+ ))}
28
+
29
+ <p>{t('conclusion')}</p>
30
+ </div>
31
+ </section>
32
+ )
33
+ }
34
+
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
3
+ import { useTranslations } from 'next-intl'
4
+
5
+ interface Tip {
6
+ title: string;
7
+ description: string;
8
+ }
9
+
10
+ export function Tips() {
11
+ const t = useTranslations('tips');
12
+ const sections = t.raw('sections') as Tip[];
13
+
14
+ const midPoint = Math.ceil(sections.length / 2);
15
+ const leftColumn = sections.slice(0, midPoint);
16
+ const rightColumn = sections.slice(midPoint);
17
+
18
+ return (
19
+ <section id="tips" className="container mx-auto px-4 py-20 bg-gray-50 dark:bg-gray-900/50 rounded-3xl my-20 border border-gray-200 dark:border-transparent">
20
+ <h2 className="text-3xl md:text-4xl font-bold text-center mb-16">
21
+ {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
22
+ </h2>
23
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-12">
24
+ {[leftColumn, rightColumn].map((column: Tip[], colIndex) => (
25
+ <div key={colIndex} className="space-y-8">
26
+ {column.map((tip: Tip, tipIndex) => (
27
+ <div key={tipIndex} className="space-y-4">
28
+ <h3 className="text-2xl font-semibold">{tip.title}</h3>
29
+ <p className="">{tip.description}</p>
30
+ </div>
31
+ ))}
32
+ </div>
33
+ ))}
34
+ </div>
35
+ </section>
36
+ )
37
+ }
38
+
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import { useTranslations } from 'next-intl'
4
+ import { globalLucideIcons as icons, getGlobalIcon } from '@base-ui/components/global-icon'
5
+
6
+ export function Usage() {
7
+ const t = useTranslations('usage');
8
+ const steps = t.raw('steps') as Array<{
9
+ title: string;
10
+ description: string;
11
+ iconKey: keyof typeof icons;
12
+ }>;
13
+
14
+ return (
15
+ <section id="usage" className="px-16 py-10 mx-16 md:mx-32">
16
+ <h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
17
+ {t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
18
+ </h2>
19
+ <p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto whitespace-nowrap">
20
+ {t('description')}
21
+ </p>
22
+ <div className="bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
23
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-12">
24
+ {steps.map((step, idx) => {
25
+ const Icon = getGlobalIcon(step.iconKey);
26
+ return (
27
+ <div key={idx} className="flex items-start">
28
+ <div className="flex-shrink-0 mr-4">
29
+ <Icon className="w-8 h-8 text-purple-500" />
30
+ </div>
31
+ <div>
32
+ <h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100 flex items-center">
33
+ {`${idx + 1}. ${step.title}`}
34
+ </h3>
35
+ <p className="text-gray-700 dark:text-gray-300">{step.description}</p>
36
+ </div>
37
+ </div>
38
+ )
39
+ })}
40
+ </div>
41
+ </div>
42
+ </section>
43
+ )
44
+ }
45
+
@@ -0,0 +1,16 @@
1
+ @layer utilities {
2
+ /* Renamed Keyframes for CTA component */
3
+ @keyframes cta-gradient-wave {
4
+ 0%, 100% {
5
+ background-position: 0% 50%;
6
+ }
7
+ 50% {
8
+ background-position: 100% 50%;
9
+ }
10
+ }
11
+
12
+ /* Renamed Utility Class for CTA component */
13
+ .animate-cta-gradient-wave {
14
+ animation: cta-gradient-wave 6s ease infinite; /* Use renamed keyframes */
15
+ }
16
+ }