diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..1778f10 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=*@nextui-org/* diff --git a/eslint.config.mjs b/eslint.config.mjs index c85fb67..29c995f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,7 +10,12 @@ const compat = new FlatCompat({ }); const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...compat.config({ + extends: ["next/core-web-vitals", "next/typescript"], + rules: { + "@typescript-eslint/no-unused-vars": "off", + }, + }), ]; export default eslintConfig; diff --git a/messages/en.json b/messages/en.json index 1fb6b93..2b4ecaf 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,7 +1,7 @@ { "GetStart": { "title": "Distribute your App with MirrorChyan", - "description": "MirrorChyan is an open-source App distribution platform that you can use to distribute your App.", + "description": "MirrorChyan is an App distribution platform that you can use to distribute your App.", "getStart": "Get Started", "apiDoc": "API Documentation" }, diff --git a/messages/zh.json b/messages/zh.json index 2cd3c1f..cd8acdf 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -1,15 +1,15 @@ { "GetStart": { "title": "使用MirrorChyan分发你的App", - "description": "MirrorChyan是一个开源的App分发平台,你可以使用它来分发你的App。", + "description": "MirrorChyan是一个App分发平台,你可以使用它来分发你的App。", "getStart": "开始使用", - "apiDoc": "API文档" + "apiDoc": "API文档", + "becomeSponsor": "到爱发电赞助" }, "GetKey": { "title": "欢迎使用MirrorChyan", "orderId": "订单号", - "getKey": "获取API Key", - "becomeSponsor": "到爱发电赞助" + "getKey": "获取API Key" }, "ShowKey": { "thanksForSponsor": "感谢您的赞助", diff --git a/package.json b/package.json index 56da0ff..5248556 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/src/app/[locale]/get-key/page.tsx b/src/app/[locale]/get-key/page.tsx index 690ac9e..e9c7cec 100644 --- a/src/app/[locale]/get-key/page.tsx +++ b/src/app/[locale]/get-key/page.tsx @@ -7,9 +7,10 @@ import { Link } from "@/i18n/routing" export default function GetKey() { const t = useTranslations('GetKey') const [orderId, setOrderId] = useState('') + return ( <> -
+

{t('title')} @@ -40,12 +41,6 @@ export default function GetKey() { - -

- - {t('becomeSponsor')} - -

diff --git a/src/app/[locale]/get-start/page.tsx b/src/app/[locale]/get-start/page.tsx index 9cd6dfd..692c14b 100644 --- a/src/app/[locale]/get-start/page.tsx +++ b/src/app/[locale]/get-start/page.tsx @@ -1,13 +1,52 @@ -import { BackgroundLines } from "@/components/BackgroundLines" +import { BackgroundBeamsWithCollision } from "@/components/BackgroundBeamsWithCollision" +import { FlipWords } from "@/components/FlipWord" import { Link } from "@/i18n/routing" +import { cn } from "@/lib/utils/css" import { useTranslations } from "next-intl" +// import { CheckIcon } from '@heroicons/react/20/solid' export default function GetStart() { const t = useTranslations('GetStart') + + const plans = [ + { + name: 'Mirror酱日卡', + price: '¥2.37', + itemId: '83f9d3b8cac611ef8fc352540025c377', + description: '感谢投喂', + tmoji: '( •̀ ω •́ )✧', + mostPopular: false, + }, + { + name: 'Mirror酱月卡', + price: '¥2.97', + itemId: '3134f94ac9aa11ef9d725254001e7c00', + description: 'Mirror酱的零食罐头', + tmoji: 'o((>ω< ))o', + mostPopular: false, + }, + { + name: 'Mirror酱季卡', + price: '¥3.87', + itemId: '9e6c7b28c9aa11efb47452540025c377', + description: 'Mirror酱的午餐盒', + tmoji: 'o(≧▽≦)o', + mostPopular: false, + }, + { + name: 'Mirror酱年卡', + price: '¥5.97', + itemId: '69c45576c9aa11ef9ace52540025c377', + description: '老板大气', + tmoji: 'ヾ(≧▽≦*)o', + mostPopular: true, + }, + ] + return ( - +
-
+

{t('title')} @@ -28,7 +67,57 @@ export default function GetStart() {

+
+ {plans.map((plan) => ( +
+

+ {plan.name} +

+
+ {plan.description} +
+

+ + {plan.price} + +

+ + {t('becomeSponsor')} + + {/*
    + {plan.features.map((feature) => ( +
  • +
  • + ))} +
*/} +
+ ))} +
-
+ ) } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 6eb7fe2..a5d53ba 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -3,6 +3,8 @@ import { getMessages } from 'next-intl/server'; import { notFound } from 'next/navigation'; import { routing } from '@/i18n/routing'; +import { Providers } from './provider'; + export default async function LocaleLayout({ children, params, @@ -21,11 +23,13 @@ export default async function LocaleLayout({ const messages = await getMessages(); return ( - - - - {children} - + + + + + {children} + + ); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1d4dfa5..273071d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,11 +11,5 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + return children; } diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..d82c1da --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation'; + +// This page only renders when the app is built statically (output: 'export') +export default function RootPage() { + redirect('/en'); +} diff --git a/src/components/BackgroundBeamsWithCollision.tsx b/src/components/BackgroundBeamsWithCollision.tsx new file mode 100644 index 0000000..011a83d --- /dev/null +++ b/src/components/BackgroundBeamsWithCollision.tsx @@ -0,0 +1,259 @@ +"use client"; +import { cn } from "@/lib/utils/css"; +import { motion, AnimatePresence } from "framer-motion"; +import React, { useRef, useState, useEffect } from "react"; + +export const BackgroundBeamsWithCollision = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + const containerRef = useRef(null); + const parentRef = useRef(null); + + const beams = [ + { + initialX: 10, + translateX: 10, + duration: 7, + repeatDelay: 3, + delay: 2, + }, + { + initialX: 600, + translateX: 600, + duration: 3, + repeatDelay: 3, + delay: 4, + }, + { + initialX: 100, + translateX: 100, + duration: 7, + repeatDelay: 7, + className: "h-6", + }, + { + initialX: 400, + translateX: 400, + duration: 5, + repeatDelay: 14, + delay: 4, + }, + { + initialX: 800, + translateX: 800, + duration: 11, + repeatDelay: 2, + className: "h-20", + }, + { + initialX: 1000, + translateX: 1000, + duration: 4, + repeatDelay: 2, + className: "h-12", + }, + { + initialX: 1200, + translateX: 1200, + duration: 6, + repeatDelay: 4, + delay: 2, + className: "h-6", + }, + ]; + + return ( +
+ {beams.map((beam) => ( + + ))} + + {children} +
+
+ ); +}; + +const CollisionMechanism = React.forwardRef< + HTMLDivElement, + { + containerRef: React.RefObject; + parentRef: React.RefObject; + beamOptions?: { + initialX?: number; + translateX?: number; + initialY?: number; + translateY?: number; + rotate?: number; + className?: string; + duration?: number; + delay?: number; + repeatDelay?: number; + }; + } +>(({ parentRef, containerRef, beamOptions = {} }, ref) => { + const beamRef = useRef(null); + const [collision, setCollision] = useState<{ + detected: boolean; + coordinates: { x: number; y: number } | null; + }>({ + detected: false, + coordinates: null, + }); + const [beamKey, setBeamKey] = useState(0); + const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false); + + useEffect(() => { + const checkCollision = () => { + if ( + beamRef.current && + containerRef.current && + parentRef.current && + !cycleCollisionDetected + ) { + const beamRect = beamRef.current.getBoundingClientRect(); + const containerRect = containerRef.current.getBoundingClientRect(); + const parentRect = parentRef.current.getBoundingClientRect(); + + if (beamRect.bottom >= containerRect.top) { + const relativeX = + beamRect.left - parentRect.left + beamRect.width / 2; + const relativeY = beamRect.bottom - parentRect.top; + + setCollision({ + detected: true, + coordinates: { + x: relativeX, + y: relativeY, + }, + }); + setCycleCollisionDetected(true); + } + } + }; + + const animationInterval = setInterval(checkCollision, 50); + + return () => clearInterval(animationInterval); + }, [cycleCollisionDetected, containerRef]); + + useEffect(() => { + if (collision.detected && collision.coordinates) { + setTimeout(() => { + setCollision({ detected: false, coordinates: null }); + setCycleCollisionDetected(false); + }, 2000); + + setTimeout(() => { + setBeamKey((prevKey) => prevKey + 1); + }, 2000); + } + }, [collision]); + + return ( + <> + + + {collision.detected && collision.coordinates && ( + + )} + + + ); +}); + +CollisionMechanism.displayName = "CollisionMechanism"; + +const Explosion = ({ ...props }: React.HTMLProps) => { + const spans = Array.from({ length: 20 }, (_, index) => ({ + id: index, + initialX: 0, + initialY: 0, + directionX: Math.floor(Math.random() * 80 - 40), + directionY: Math.floor(Math.random() * -50 - 10), + })); + + return ( +
+ + {spans.map((span) => ( + + ))} +
+ ); +}; diff --git a/src/components/FlipWord.tsx b/src/components/FlipWord.tsx new file mode 100644 index 0000000..9c7d3f5 --- /dev/null +++ b/src/components/FlipWord.tsx @@ -0,0 +1,98 @@ +"use client"; +import React, { useCallback, useEffect, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { cn } from "@/lib/utils/css"; + +export const FlipWords = ({ + words, + duration = 3000, + className, +}: { + words: string[]; + duration?: number; + className?: string; +}) => { + const [currentWord, setCurrentWord] = useState(words[0]); + const [isAnimating, setIsAnimating] = useState(false); + + // 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]); + + return ( + { + setIsAnimating(false); + }} + > + + {/* edit suggested by Sajal: https://x.com/DewanganSajal */} + {currentWord.split(" ").map((word, wordIndex) => ( + + {word.split("").map((letter, letterIndex) => ( + + {letter} + + ))} +   + + ))} + + + ); +}; diff --git a/tailwind.config.js b/tailwind.config.js index 3efb45f..dd2eed5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,6 @@ import { default as flattenColorPalette } from "tailwindcss/lib/util/flattenColorPalette"; import daisyui from "daisyui"; +import { nextui } from "@nextui-org/react"; /** @type {import('tailwindcss').Config} */ export const content = [ @@ -9,12 +10,13 @@ export const content = [ // Or if using `src` directory: "./src/**/*.{js,ts,jsx,tsx,mdx}", + "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}" ]; export const darkMode = "class"; export const theme = { extend: {}, }; -export const plugins = [addVariablesForColors, daisyui]; +export const plugins = [addVariablesForColors, daisyui, nextui()]; // This plugin adds each Tailwind color as a global CSS variable, e.g. var(--gray-200). function addVariablesForColors({ addBase, theme }) {