diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ff73a7e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/README.md b/README.md index 89925c5..ee51f42 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Toban のランディングページプロジェクトです。 - **Emotion** - CSS-in-JS ライブラリ - **Biome** - リンター・フォーマッター - **Yarn** - パッケージマネージャー +- **React Icons** - アイコンライブラリ +- **Storybook** - コンポーネント開発環境 +- **serve** - 静的ファイル配信サーバー ## 📋 必要条件 @@ -30,7 +33,15 @@ cd toban-lp yarn install ``` -### 3. 環境変数の設定 +### 3. 不足している依存関係のインストール + +プロジェクトで使用されているアイコンライブラリをインストールします: + +```bash +yarn add react-icons +``` + +### 4. 環境変数の設定 必要に応じて環境変数を設定してください: @@ -63,9 +74,38 @@ yarn build yarn start ``` +### 静的ファイルのエクスポート + +このプロジェクトは静的サイトとしてビルドされます: + +```bash +yarn export +``` + +### Storybookの起動 + +コンポーネントの開発・テスト用にStorybookを使用できます: + +```bash +yarn build-storybook +``` + ## 🧹 コード品質 -### リント +### 自動フォーマット + +このプロジェクトでは以下の方法でコード品質が自動的に管理されます: + +#### 1. Git Hooks(推奨) +- コミット前に自動的にBiomeのチェックと修正が実行されます +- 問題がある場合はコミットがブロックされます +- `lint-staged`により、ステージされたファイルのみが処理されます + +#### 2. VS Code設定 +- ファイル保存時に自動フォーマットが適用されます +- Biome拡張機能が推奨されます + +#### 3. 手動実行 ```bash # リントチェック @@ -73,48 +113,88 @@ yarn lint # リント自動修正 yarn lint:fix -``` - -### フォーマット -```bash # コードフォーマット yarn format -``` -### 総合チェック - -```bash -# リント + フォーマットの総合チェック +# 総合チェック(リント + フォーマット) yarn check # 総合チェック + 自動修正 -yarn clean +yarn check:fix + +# 型チェック +yarn type-check ``` +### 開発時の注意点 + +- コミット前に必ず`yarn check:fix`を実行することを推奨します +- VS CodeでBiome拡張機能をインストールすると、リアルタイムでエラーが表示されます +- プッシュ前にGitHub ActionsでBiomeチェックが実行されます + ## 📁 プロジェクト構造 ``` toban-lp/ ├── public/ # 静的ファイル │ └── assets/ # アセットファイル +│ ├── logo/ # ロゴファイル +│ ├── HowItWorksImage/ # 説明画像 +│ ├── toban-logo.svg # Tobanロゴ +│ ├── toban-logo-text.svg # Tobanテキストロゴ +│ ├── hero-image.png # ヒーロー画像 +│ └── favicon.ico # ファビコン ├── src/ # ソースコード │ ├── components/ # React コンポーネント │ │ ├── common/ # 共通コンポーネント +│ │ │ └── Button.tsx # ボタンコンポーネント │ │ ├── layouts/ # レイアウトコンポーネント +│ │ │ └── base.tsx # ベースレイアウト │ │ └── organisms/ # 複合コンポーネント +│ │ ├── Header/ # ヘッダーコンポーネント +│ │ ├── Footer/ # フッターコンポーネント +│ │ ├── HeroSection.tsx # ヒーローセクション +│ │ ├── Features.tsx # 機能紹介 +│ │ ├── ProblemSolution.tsx # 問題と解決策 +│ │ ├── HowItWorks.tsx # 使い方説明 +│ │ ├── UseCases.tsx # ユースケース +│ │ ├── CaseStudies.tsx # 事例紹介 +│ │ ├── SecurityStack.tsx # セキュリティスタック +│ │ ├── TrackRecord.tsx # 実績 +│ │ ├── Pricing.tsx # 料金プラン +│ │ ├── Faq.tsx # よくある質問 +│ │ ├── AwardsMedia.tsx # 受賞・メディア +│ │ ├── InfiniteLoop.tsx # 無限ループ +│ │ └── GettingStarted.tsx # 始め方 │ ├── data/ # データファイル │ ├── hooks/ # カスタムフック │ ├── locales/ # 国際化ファイル │ ├── pages/ # Next.js ページ +│ │ ├── _app.tsx # アプリケーション設定 +│ │ ├── _document.tsx # ドキュメント設定 +│ │ └── index.tsx # メインページ │ ├── themes/ # テーマ設定 -│ │ ├── settings/ # テーマ設定 -│ │ └── styles/ # スタイル定義 +│ │ ├── settings/ # テーマ設定 +│ │ │ ├── color.ts # カラー設定 +│ │ │ ├── spaces.ts # スペーシング設定 +│ │ │ └── breakpoints.ts # ブレークポイント設定 +│ │ ├── styles/ # スタイル定義 +│ │ │ └── globals.css # グローバルスタイル +│ │ └── global.ts # グローバルテーマ設定 │ └── types/ # TypeScript 型定義 +├── .github/ # GitHub設定 +├── .yarn/ # Yarn設定 ├── biome.json # Biome 設定 ├── next.config.ts # Next.js 設定 +├── next-env.d.ts # Next.js型定義 ├── package.json # パッケージ設定 -└── tsconfig.json # TypeScript 設定 +├── tsconfig.json # TypeScript 設定 +├── tsconfig.tsbuildinfo # TypeScriptビルド情報 +├── .gitignore # Git除外設定 +├── .gitattributes # Git属性設定 +├── .yarnrc.yml # Yarn設定 +└── yarn.lock # Yarnロックファイル ``` ## 🤝 コントリビューション @@ -135,3 +215,5 @@ toban-lp/ - [TypeScript ドキュメント](https://www.typescriptlang.org/docs/) - [Emotion ドキュメント](https://emotion.sh/docs/introduction) - [Biome ドキュメント](https://biomejs.dev/) +- [Storybook ドキュメント](https://storybook.js.org/docs/react/get-started/introduction) +- [React Icons ドキュメント](https://react-icons.github.io/react-icons/) diff --git a/next.config.ts b/next.config.ts index a49ebbb..ec27dff 100644 --- a/next.config.ts +++ b/next.config.ts @@ -13,9 +13,7 @@ const nextConfig: NextConfig = { }, env: { - NEXT_PUBLIC_AIRTABLE_PAT: process.env.NEXT_PUBLIC_AIRTABLE_PAT, - NEXT_PUBLIC_AIRTABLE_BASE: process.env.NEXT_PUBLIC_AIRTABLE_BASE, - NEXT_PUBLIC_AIRTABLE_TABLE: process.env.NEXT_PUBLIC_AIRTABLE_TABLE, + }, }; diff --git a/package.json b/package.json index 48421ca..384b846 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,12 @@ "build": "next build", "export": "next build && next export", "start": "serve out", - "lint": "yarn run biome lint ./src", - "lint:fix": "yarn run biome lint ./src --write", - "lint:fix:css": "stylelint --fix \"**/*.{js,jsx,ts,tsx}\"", - "format": "yarn run biome format ./src --write", - "check": "yarn run biome check ./src", - "clean": "yarn run biome check ./src --write", + "lint": "biome lint ./src", + "lint:fix": "biome lint ./src --write", + "format": "biome format ./src --write", + "check": "biome check ./src", + "check:fix": "biome check --write --unsafe ./src", + "type-check": "tsc --noEmit", "build-storybook": "storybook build" }, "dependencies": { @@ -21,6 +21,7 @@ "next": "^15.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", + "react-icons": "^5.5.0", "serve": "^14.0.0" }, "devDependencies": { @@ -31,6 +32,12 @@ "lint-staged": "^15.0.0", "typescript": "^5.0.0" }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "biome check --write --unsafe ./src", + "biome format --write ./src" + ] + }, "simple-git-hooks": { "pre-commit": "npx lint-staged" }, diff --git a/public/assets/HowItWorksImage/HowItWorks1.png b/public/assets/HowItWorksImage/HowItWorks1.png new file mode 100644 index 0000000..7d0c5ae Binary files /dev/null and b/public/assets/HowItWorksImage/HowItWorks1.png differ diff --git a/public/assets/HowItWorksImage/HowItWorks2.png b/public/assets/HowItWorksImage/HowItWorks2.png new file mode 100644 index 0000000..99fce9a Binary files /dev/null and b/public/assets/HowItWorksImage/HowItWorks2.png differ diff --git a/public/assets/HowItWorksImage/HowItWorks3-1.png b/public/assets/HowItWorksImage/HowItWorks3-1.png new file mode 100644 index 0000000..701796a Binary files /dev/null and b/public/assets/HowItWorksImage/HowItWorks3-1.png differ diff --git a/public/assets/HowItWorksImage/HowItWorks3-2.png b/public/assets/HowItWorksImage/HowItWorks3-2.png new file mode 100644 index 0000000..65ee011 Binary files /dev/null and b/public/assets/HowItWorksImage/HowItWorks3-2.png differ diff --git a/public/assets/HowItWorksImage/HowItWorks4.png b/public/assets/HowItWorksImage/HowItWorks4.png new file mode 100644 index 0000000..68d6694 Binary files /dev/null and b/public/assets/HowItWorksImage/HowItWorks4.png differ diff --git a/public/assets/HowItWorksImage/HowItWorks5.png b/public/assets/HowItWorksImage/HowItWorks5.png new file mode 100644 index 0000000..9691c69 Binary files /dev/null and b/public/assets/HowItWorksImage/HowItWorks5.png differ diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico new file mode 100644 index 0000000..b570f98 Binary files /dev/null and b/public/assets/favicon.ico differ diff --git a/public/assets/hero-image.png b/public/assets/hero-image.png new file mode 100644 index 0000000..42ced90 Binary files /dev/null and b/public/assets/hero-image.png differ diff --git a/public/assets/logo/ETHTokyoLogoBlack.png b/public/assets/logo/ETHTokyoLogoBlack.png new file mode 100644 index 0000000..37ad6b8 Binary files /dev/null and b/public/assets/logo/ETHTokyoLogoBlack.png differ diff --git a/public/assets/logo/comoris-logo.png b/public/assets/logo/comoris-logo.png new file mode 100644 index 0000000..0f058a8 Binary files /dev/null and b/public/assets/logo/comoris-logo.png differ diff --git a/public/assets/logo/ens-mark-Blue.svg b/public/assets/logo/ens-mark-Blue.svg new file mode 100644 index 0000000..130b6c0 --- /dev/null +++ b/public/assets/logo/ens-mark-Blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/logo/fracton-logo.png b/public/assets/logo/fracton-logo.png new file mode 100644 index 0000000..7db115d Binary files /dev/null and b/public/assets/logo/fracton-logo.png differ diff --git a/public/assets/logo/hats-logo.png b/public/assets/logo/hats-logo.png new file mode 100644 index 0000000..f2c1473 Binary files /dev/null and b/public/assets/logo/hats-logo.png differ diff --git a/public/assets/logo/localcoop-logo.png b/public/assets/logo/localcoop-logo.png new file mode 100644 index 0000000..88004a6 Binary files /dev/null and b/public/assets/logo/localcoop-logo.png differ diff --git a/public/assets/logo/shiojiridao-kogo.webp b/public/assets/logo/shiojiridao-kogo.webp new file mode 100644 index 0000000..c4db5fc Binary files /dev/null and b/public/assets/logo/shiojiridao-kogo.webp differ diff --git a/public/assets/logo/splits-logo.png b/public/assets/logo/splits-logo.png new file mode 100644 index 0000000..bf10174 Binary files /dev/null and b/public/assets/logo/splits-logo.png differ diff --git a/public/assets/toban-logo-text.svg b/public/assets/toban-logo-text.svg new file mode 100644 index 0000000..cc1adba --- /dev/null +++ b/public/assets/toban-logo-text.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/toban-logo.svg b/public/assets/toban-logo.svg new file mode 100644 index 0000000..3adb2ba --- /dev/null +++ b/public/assets/toban-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx new file mode 100644 index 0000000..78a9b24 --- /dev/null +++ b/src/components/common/Button.tsx @@ -0,0 +1,154 @@ +import { mq } from "@/themes/settings/breakpoints"; +import { brand, neutral, themeLight } from "@/themes/settings/color"; +import { type SerializedStyles, css } from "@emotion/react"; +import Link from "next/link"; +import type { ReactNode } from "react"; + +type ButtonVariant = "primary" | "secondary" | "outline" | "disabled"; +type ButtonSize = "small" | "medium" | "large"; + +export interface ButtonProps { + children: ReactNode; + variant?: ButtonVariant; + size?: ButtonSize; + icon?: ReactNode; + href?: string; + external?: boolean; + onClick?: () => void; + className?: string; + type?: "button" | "submit" | "reset"; + css?: SerializedStyles; +} + +const Button = ({ + children, + variant = "primary", + size = "medium", + icon, + href, + external = false, + onClick, + className, + type = "button", + css: cssProp, +}: ButtonProps) => { + const baseStyle = css` + align-items: center; + border: none; + border-radius: 9999px; + cursor: pointer; + display: inline-flex; + font-weight: 600; + gap: 0.5rem; + justify-content: center; + text-decoration: none; + transition: background-color 0.2s ease; + `; + + const variantStyles = { + primary: css` + background-color: ${brand.Primary}; + color: ${neutral.White}; + &:hover { + background-color: ${themeLight.PrimaryHover}; + } + `, + secondary: css` + background-color: ${brand.Secondary}; + color: ${neutral.White}; + cursor: not-allowed; + `, + outline: css` + background-color: transparent; + border: 2px solid ${brand.Primary}; + color: ${brand.Primary}; + &:hover { + background-color: ${themeLight.PrimaryLowContrast}; + } + `, + disabled: css` + background-color: ${neutral.Grey3}; + color: ${neutral.White}; + cursor: not-allowed; + `, + }; + + const sizeStyles = { + small: css` + font-size: 0.875rem; + padding: 0.5rem 1.25rem; + `, + medium: css` + font-size: 1rem; + padding: 0.75rem 1.75rem; + ${mq.tablet} { + padding: 0.875rem 2rem; + } + `, + large: css` + font-size: 1.125rem; + padding: 1rem 2.25rem; + ${mq.tablet} { + font-size: 1.25rem; + padding: 1.125rem 2.5rem; + } + `, + }; + + const buttonStyle = css` + ${baseStyle} + ${variantStyles[variant]} + ${sizeStyles[size]} + ${cssProp} + `; + + const content = ( + <> + {children} + {icon && {icon}} + + ); + + if (href && external) { + return ( + + {content} + + ); + } + + if (href) { + return ( + + {content} + + ); + } + + // 通常ボタン + return ( + + ); +}; + +export default Button; diff --git a/src/components/layouts/base.tsx b/src/components/layouts/base.tsx new file mode 100644 index 0000000..78688af --- /dev/null +++ b/src/components/layouts/base.tsx @@ -0,0 +1,126 @@ +import Footer from "@/components/organisms/Footer/index"; +import Header from "@/components/organisms/Header/index"; +import { globalStyles } from "@/themes/global"; +import { mq } from "@/themes/settings/breakpoints"; +import { brand, neutral, themeLight } from "@/themes/settings/color"; +import type { PageProps } from "@/types"; +import { Global, css } from "@emotion/react"; +import { Inter } from "next/font/google"; +import Head from "next/head"; +import type { FC } from "react"; + +export const metadata = { + title: { + template: "%s | Toban", + default: "Toban", + }, + description: "Toban 当番 いちばん簡単な貢献の記録と報酬の分配", + keywords: ["Toban", "当番", "協働ツール", "Blockchain", "WEB3"], + category: "technology", + authors: [{ name: "Toban", url: "https://github.com/hackdays-io/toban" }], + formatDetection: { + email: false, + address: false, + telephone: false, + }, + // openGraph: { + // title: "ETHTokyo 2025", + // description: "The Japanese Ethereum Community Hackathon & Conference", + // url: "https://ethtokyo.org/", + // siteName: "ethtokyo.org", + // images: [ + // { + // url: "https://ethtokyo.org/assets/ETHTokyoLogo.png", + // width: 800, + // height: 600, + // }, + // ], + // locale: "en_EN", + // type: "website", + // }, + // twitter: { + // card: "summary_large_image", + // title: "ETHTokyo 2025", + // description: "The Japanese Ethereum Community Hackathon & Conference", + // siteId: "1511737631948034048", + // creator: "@Ethereum_JP", + // creatorId: "1511737631948034048", + // images: ["https://ethtokyo.org/assets/ETHTokyoLogo.png"], + // }, + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#B8FAF6" }, + { media: "(prefers-color-scheme: dark)", color: "#C9B3F5" }, + ], + icons: { + icon: "/assets/toban-logo.svg", + }, + robots: { + index: true, + follow: true, + nocache: true, + googleBot: { + index: true, + follow: true, + noimageindex: true, + "max-image-preview": "large", + "max-video-preview": -1, + "max-snippet": -1, + }, + }, +}; + +const fontInter = Inter({ + subsets: ["latin"], + display: "swap", +}); + +// const styleCache = createCache({ key: 'next' }); + +const Layout: FC = ({ pageTitle, children }) => { + const siteTitle = "Toban"; + const baseLayoutStyle = css` + background-color: ${themeLight.Background}; + color: ${neutral.Text}; + display: flex; + flex-direction: column; + font-family: 'Inter', sans-serif; + min-height: 100vh; + + a { + color: ${themeLight.Link}; + text-decoration: none; + transition: color 0.3s ease; + + &:hover { + color: ${themeLight.LinkHover}; + } + } + `; + + const mainLayoutStyle = css` + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + `; + + return ( + <> + + + {/* HTML header */} + + {pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle} + + + {/* main body */} +
+
+
{children}
+
+
+ + ); +}; + +export default Layout; diff --git a/src/components/organisms/AwardsMedia.tsx b/src/components/organisms/AwardsMedia.tsx new file mode 100644 index 0000000..807df19 --- /dev/null +++ b/src/components/organisms/AwardsMedia.tsx @@ -0,0 +1,416 @@ +"use client"; + +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function AwardsMedia() { + const [isVisible, setIsVisible] = useState(false); + const [activeItem, setActiveItem] = useState(null); + + const awards = [ + { + id: "eth-tokyo-2024", + title: "ETH Tokyo 2024 Finalist", + subtitle: "(Toban プロトタイプ)", + date: "2024年4月", + type: "award", + icon: "🏆", + color: "#ff6b6b", + links: [ + { + url: "https://www.linkedin.com/posts/haruki-kondo-517073204_selected-as-a-finalist-for-eth-tokyo-2024-activity-7233684980688642048-GJIh?utm_source=chatgpt.com", + text: "linkedin.com", + }, + ], + }, + { + id: "agentic-ethereum-2025", + title: "Agentic Ethereum 2025", + subtitle: "ハッカソン参加", + date: "2025年1月", + type: "event", + icon: "🚀", + color: "#4ecdc4", + links: [ + { + url: "https://ethglobal.com/events/agents?utm_source=chatgpt.com", + text: "ethglobal.com", + }, + ], + }, + { + id: "fracton-incubation-2024", + title: "Fracton Incubation 2024", + subtitle: "採択", + date: "2024年6月", + type: "program", + icon: "🌱", + color: "#45b7d1", + links: [ + { + url: "https://technode.global/2023/03/22/fracton-ventures-shaping-the-web3-landscape-through-strategic-incubation-and-global-collaboration-qa/?utm_source=chatgpt.com", + text: "technode.global", + }, + ], + }, + { + id: "ethtaipei-2025", + title: "ETHTaipei 2025", + subtitle: "スピーカー登壇", + date: "2025年1月", + type: "speaking", + icon: "🎤", + color: "#8b5cf6", + links: [ + { + url: "https://www.dlnews.com/research/internal/ethtaipei-2025-partners-with-ethglobal-vitalik-buterin/?utm_source=chatgpt.com", + text: "ethtaipei.org", + }, + ], + }, + { + id: "weekly-gm-joichi-ito", + title: '週刊番組 "weekly gm" 出演', + subtitle: "(Joichi Ito YouTube)", + date: "2024年12月", + type: "media", + icon: "📺", + color: "#f59e0b", + links: [ + { + url: "https://dalab.xyz/en/project/weekly-gm/?utm_source=chatgpt.com", + text: "dalab.xyz", + }, + ], + }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("awards-media"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + const getTypeLabel = (type: string) => { + const typeMap: { [key: string]: string } = { + award: "受賞", + event: "イベント", + program: "プログラム", + speaking: "講演", + media: "メディア", + }; + return typeMap[type] || type; + }; + + return ( +
+
+
+

+ 受賞 & メディア掲載 +

+

+ Tobanプロジェクトの主要な実績とマイルストーン +

+
+ +
+ {/* タイムライン軸 */} +
+ + {awards.map((award, index) => ( +
setActiveItem(index)} + onMouseLeave={() => setActiveItem(null)} + onTouchStart={() => setActiveItem(index)} + onTouchEnd={() => setTimeout(() => setActiveItem(null), 2000)} + > + {/* タイムライン ドット */} +
+ {award.icon} +
+ + {/* コンテンツカード */} +
+ {/* ヘッダー */} +
+
+ {getTypeLabel(award.type)} +
+
+ {award.date} +
+
+ + {/* タイトル */} +

+ {award.title} +

+

+ {award.subtitle} +

+ + {/* リンク */} + +
+
+ ))} +
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + color: "white", + position: "relative", + overflow: "hidden", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1000px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + margin: "0 0 15px 0", + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #ff6b6b, #4ecdc4)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const subtitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "rgba(255, 255, 255, 0.8)", + margin: 0, + fontWeight: "400", +}; + +const timelineStyle: React.CSSProperties = { + position: "relative", + paddingLeft: "60px", +}; + +const timelineLineStyle: React.CSSProperties = { + position: "absolute", + left: "30px", + top: "40px", + bottom: "40px", + width: "4px", + background: + "linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1))", + borderRadius: "2px", +}; + +const timelineItemStyle: React.CSSProperties = { + position: "relative", + marginBottom: "40px", + display: "flex", + alignItems: "flex-start", + opacity: 0, + transform: "translateX(-50px)", + transition: "all 0.6s cubic-bezier(0.4, 0, 0.2, 1)", +}; + +const timelineDotStyle: React.CSSProperties = { + position: "absolute", + left: "-45px", + top: "20px", + width: "30px", + height: "30px", + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 2, +}; + +const dotIconStyle: React.CSSProperties = { + fontSize: "0.9rem", + filter: "drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))", + backgroundColor: "white", + borderRadius: "50%", + width: "30px", + height: "30px", + display: "flex", + alignItems: "center", + justifyContent: "center", +}; + +const cardStyle: React.CSSProperties = { + backgroundColor: "white", + borderRadius: "16px", + padding: "30px", + borderLeft: "4px solid", + boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", + cursor: "pointer", + width: "100%", + marginLeft: "20px", +}; + +const cardHeaderStyle: React.CSSProperties = { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "20px", +}; + +const typeBadgeStyle: React.CSSProperties = { + padding: "6px 12px", + borderRadius: "12px", + fontSize: "0.8rem", + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: "0.05em", +}; + +const dateStyle: React.CSSProperties = { + fontSize: "0.9rem", + color: "#64748b", + fontWeight: "500", +}; + +const awardTitleStyle: React.CSSProperties = { + fontSize: "1.3rem", + fontWeight: "600", + margin: "0 0 8px 0", + lineHeight: 1.3, + transition: "color 0.3s ease", +}; + +const awardSubtitleStyle: React.CSSProperties = { + fontSize: "1rem", + color: "#64748b", + margin: "0 0 20px 0", + lineHeight: 1.4, +}; + +const linksContainerStyle: React.CSSProperties = { + display: "flex", + gap: "12px", + flexWrap: "wrap", +}; + +const linkButtonStyle: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: "8px", + padding: "10px 16px", + borderRadius: "8px", + border: "2px solid", + fontSize: "0.9rem", + fontWeight: "600", + textDecoration: "none", + transition: "all 0.3s ease", + backgroundColor: "transparent", +}; + +const linkIconStyle: React.CSSProperties = { + fontSize: "0.8rem", +}; diff --git a/src/components/organisms/CaseStudies.tsx b/src/components/organisms/CaseStudies.tsx new file mode 100644 index 0000000..d0fc229 --- /dev/null +++ b/src/components/organisms/CaseStudies.tsx @@ -0,0 +1,424 @@ +"use client"; + +import Image from "next/image"; +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function CaseStudies() { + const [isVisible, setIsVisible] = useState(false); + const [hoveredCard, setHoveredCard] = useState(null); + + const cases = [ + { + id: "comoris", + name: "Comoris", + logoSrc: "/assets/logo/comoris-logo.png", + description: "沖縄・コミュニティ DAO。子育て支援トークン導入", + link: "Case Study →", + linkUrl: "#", + color: "#ff6b6b", + category: "コミュニティDAO", + status: "active", + }, + { + id: "shiojiri-dao", + name: "塩尻DAO", + logoSrc: "/assets/logo/shiojiridao-kogo.webp", + description: "長野県・地域活性 DAO。農業バウンティを自動分配", + link: "Interview →", + linkUrl: "#", + color: "#4ecdc4", + category: "地域DAO", + status: "active", + }, + { + id: "ento", + name: "ento", + logoSrc: null, + description: "オープンソース気象データ連携", + link: "Coming soon", + linkUrl: null, + color: "#45b7d1", + category: "オープンソース", + status: "coming", + }, + { + id: "eth-tokyo", + name: "ETH Tokyo", + logoSrc: "/assets/logo/ETHTokyoLogoBlack.png", + description: "ハッカソン採択プロジェクト", + link: "Highlights →", + linkUrl: "#", + color: "#8b5cf6", + category: "ハッカソン", + status: "active", + }, + { + id: "fracton", + name: "Fracton", + logoSrc: "/assets/logo/fracton-logo.png", + description: "インキュベーション支援", + link: "Article →", + linkUrl: "#", + color: "#f59e0b", + category: "インキュベーター", + status: "active", + }, + { + id: "localcoop", + name: "Localcoop", + logoSrc: "/assets/logo/localcoop-logo.png", + description: "助け合いプラットフォーム", + link: "Coming soon", + linkUrl: null, + color: "#10b981", + category: "プラットフォーム", + status: "coming", + }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.1, rootMargin: "50px" }, + ); + + const element = document.getElementById("case-studies"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + return ( +
+
+
+

+ 導入事例 +

+

+ 実際にTobanを活用している組織とプロジェクト +

+
+ +
+ {cases.map((caseItem, index) => ( +
setHoveredCard(index)} + onMouseLeave={() => setHoveredCard(null)} + > + {/* ステータスバッジ */} +
+ {caseItem.status === "active" ? "運用中" : "近日公開"} +
+ + {/* カテゴリー */} +
+ {caseItem.category} +
+ + {/* ロゴセクション */} +
+ {caseItem.logoSrc ? ( + {`${caseItem.name} + ) : ( +
+ {caseItem.name[0]} +
+ )} +
+ + {/* コンテンツ */} +
+

+ {caseItem.name} +

+

+ {caseItem.description} +

+
+ + {/* フッター */} +
+ {caseItem.linkUrl ? ( + + {caseItem.link} + + ) : ( + + {caseItem.link} + + )} +
+ + {/* ホバー時のアクセント */} + {hoveredCard === index && ( +
+ )} +
+ ))} +
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)", + minHeight: "100vh", + display: "flex", + alignItems: "center", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.5rem)", + fontWeight: "700", + color: "#1e293b", + margin: "0 0 15px 0", + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #ff6b6b, #4ecdc4)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const subtitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "#64748b", + margin: 0, + fontWeight: "400", +}; + +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", + gap: "clamp(20px, 4vw, 30px)", + maxWidth: "1100px", + margin: "0 auto", +}; + +const cardStyle: React.CSSProperties = { + padding: "clamp(20px, 4vw, 30px)", + borderRadius: "20px", + backgroundColor: "white", + border: "1px solid #e2e8f0", + boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", + cursor: "pointer", + position: "relative", + overflow: "hidden", + opacity: 0, + transform: "translateY(20px)", +}; + +const statusBadgeStyle: React.CSSProperties = { + position: "absolute", + top: "20px", + right: "20px", + padding: "4px 12px", + borderRadius: "12px", + fontSize: "0.75rem", + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: "0.05em", +}; + +const categoryStyle: React.CSSProperties = { + display: "inline-block", + padding: "6px 12px", + borderRadius: "8px", + fontSize: "0.8rem", + fontWeight: "600", + marginBottom: "20px", + letterSpacing: "0.05em", +}; + +const logoSectionStyle: React.CSSProperties = { + width: "100px", + height: "100px", + borderRadius: "16px", + border: "2px solid", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: "25px", + marginLeft: "auto", + marginRight: "auto", + transition: "border-color 0.3s ease", +}; + +const logoPlaceholderStyle: React.CSSProperties = { + width: "60px", + height: "60px", + borderRadius: "12px", + display: "flex", + alignItems: "center", + justifyContent: "center", + fontWeight: "bold", +}; + +const logoTextStyle: React.CSSProperties = { + fontSize: "1.5rem", + fontWeight: "700", +}; + +const contentSectionStyle: React.CSSProperties = { + marginBottom: "25px", +}; + +const nameStyle: React.CSSProperties = { + fontSize: "1.4rem", + fontWeight: "600", + margin: "0 0 15px 0", + lineHeight: 1.3, + transition: "color 0.3s ease", +}; + +const descriptionStyle: React.CSSProperties = { + fontSize: "1rem", + lineHeight: 1.6, + margin: 0, + transition: "color 0.3s ease", +}; + +const footerStyle: React.CSSProperties = { + paddingTop: "20px", + borderTop: "1px solid", + transition: "border-color 0.3s ease", +}; + +const linkStyle: React.CSSProperties = { + display: "inline-block", + padding: "10px 20px", + borderRadius: "8px", + border: "2px solid", + fontSize: "0.9rem", + fontWeight: "600", + textDecoration: "none", + transition: "all 0.3s ease", + backgroundColor: "transparent", +}; + +const comingSoonStyle: React.CSSProperties = { + display: "inline-block", + padding: "10px 20px", + borderRadius: "8px", + fontSize: "0.9rem", + fontWeight: "600", + fontStyle: "italic", +}; + +const accentLineStyle: React.CSSProperties = { + position: "absolute", + top: 0, + left: 0, + width: "4px", + height: "100%", + transition: "all 0.3s ease", +}; diff --git a/src/components/organisms/Faq.tsx b/src/components/organisms/Faq.tsx new file mode 100644 index 0000000..055a0d8 --- /dev/null +++ b/src/components/organisms/Faq.tsx @@ -0,0 +1,427 @@ +"use client"; + +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function Faq() { + const [isVisible, setIsVisible] = useState(false); + const [openIndex, setOpenIndex] = useState(0); // 最初の質問を開いておく + + const faqs = [ + { + id: "technical-knowledge", + question: "Tobanを使うのに専門的な知識は必要ですか?", + answer: "いいえ、必要ありません。", + detailedAnswer: + "Tobanはブラウザベースで利用でき、NFTやブロックチェーンの知識がなくても簡単に使い始められます。技術的な部分はすべてTobanが裏側で処理します。", + icon: "🔰", + color: "#3b82f6", + }, + { + id: "free-usage", + question: "無料で使えますか?", + answer: "はい。まずは無料プランで始められます。", + detailedAnswer: + "小規模コミュニティや実証実験レベルでは無料で十分ご利用いただけます。利用規模が大きくなった場合に応じて有料プランをご用意しています。", + icon: "💰", + color: "#10b981", + }, + { + id: "reward-types", + question: "報酬は必ず金銭で支払う必要がありますか?", + answer: "いいえ。", + detailedAnswer: + "Tobanでは「金銭」「ポイント」「NFT」「特典(イベント参加権、商品引換など)」など、コミュニティに合った形で報酬を設計できます。", + icon: "🎁", + color: "#f59e0b", + }, + { + id: "security", + question: "セキュリティは大丈夫ですか?", + answer: "はい。", + detailedAnswer: + "Tobanはスマートコントラクトや暗号化技術を用いた堅牢なプロトコルスタックで構築されており、改ざんや不正を防ぎます。大切なデータは安全に保護されます。", + icon: "🔒", + color: "#8b5cf6", + }, + { + id: "community-types", + question: "どんなコミュニティに向いていますか?", + answer: + "自治体・企業・NPO・地域団体・オンラインサロンなど、大小さまざまなコミュニティに対応可能です。", + detailedAnswer: + "特に「貢献を見える化して公平に報酬や感謝を伝えたい」場面で効果を発揮します。", + icon: "🏘️", + color: "#ef4444", + }, + { + id: "data-usage", + question: "活動データはどのように活用できますか?", + answer: + "貢献ログはTobanのダッシュボードで集計・可視化され、助成金申請や活動報告、企業のROI測定などに活用できます。", + detailedAnswer: "透明で信頼できるデータとして第三者にも提示可能です。", + icon: "📊", + color: "#06b6d4", + }, + { + id: "account-requirements", + question: "参加者は特別なアカウントやウォレットが必要ですか?", + answer: "いいえ。", + detailedAnswer: + "メールアドレスやSNSアカウントで簡単に参加できます。必要に応じて後からウォレットを接続することも可能です。", + icon: "👤", + color: "#84cc16", + }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("faq"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + const toggleFaq = (index: number) => { + setOpenIndex(openIndex === index ? null : index); + }; + + return ( +
+
+
+

+ よくある質問 +

+

+ Tobanについて寄せられる質問とその回答をまとめました +

+
+ +
+ {faqs.map((faq, index) => ( +
+ + +
+
+

+ + A. + + {faq.answer} +

+

{faq.detailedAnswer}

+
+
+
+ ))} +
+ + {/* 追加サポート */} +
+

他にご質問がありますか?

+

+ お気軽にお問い合わせください。コミュニティでお待ちしています。 +

+ +
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #1e293b 0%, #334155 100%)", + color: "white", + minHeight: "100vh", + display: "flex", + alignItems: "center", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "800px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + margin: "0 0 15px 0", + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #3b82f6, #10b981)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const subtitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "rgba(255, 255, 255, 0.8)", + margin: 0, + fontWeight: "400", +}; + +const faqContainerStyle: React.CSSProperties = { + marginBottom: "60px", +}; + +const faqItemStyle: React.CSSProperties = { + marginBottom: "20px", + borderRadius: "16px", + overflow: "hidden", + backgroundColor: "white", + boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", + opacity: 0, + transform: "translateY(30px)", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", +}; + +const questionButtonStyle: React.CSSProperties = { + width: "100%", + padding: "clamp(20px, 4vw, 25px) clamp(20px, 4vw, 30px)", + border: "2px solid", + background: "white", + cursor: "pointer", + transition: "all 0.3s ease", + borderRadius: 0, +}; + +const questionHeaderStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "clamp(12px, 3vw, 20px)", +}; + +const iconWrapperStyle: React.CSSProperties = { + width: "clamp(40px, 8vw, 50px)", + height: "clamp(40px, 8vw, 50px)", + borderRadius: "12px", + border: "2px solid", + display: "flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, +}; + +const iconStyle: React.CSSProperties = { + fontSize: "clamp(1.2rem, 3vw, 1.5rem)", +}; + +const questionTextStyle: React.CSSProperties = { + fontSize: "clamp(0.95rem, 2.5vw, 1.1rem)", + fontWeight: "600", + textAlign: "left", + flex: 1, + transition: "color 0.3s ease", +}; + +const chevronStyle: React.CSSProperties = { + fontSize: "0.8rem", + transition: "transform 0.3s ease, color 0.3s ease", + flexShrink: 0, +}; + +const answerContainerStyle: React.CSSProperties = { + overflow: "hidden", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", + backgroundColor: "#f8fafc", +}; + +const answerContentStyle: React.CSSProperties = { + borderTop: "1px solid #e2e8f0", +}; + +const shortAnswerStyle: React.CSSProperties = { + fontSize: "1rem", + color: "#1e293b", + margin: "0 0 15px 0", + fontWeight: "600", + display: "flex", + alignItems: "flex-start", + gap: "10px", + textAlign: "left", +}; + +const answerIconStyle: React.CSSProperties = { + fontSize: "1rem", + fontWeight: "bold", + flexShrink: 0, +}; + +const detailedAnswerStyle: React.CSSProperties = { + fontSize: "0.95rem", + color: "#64748b", + margin: 0, + lineHeight: 1.6, + textAlign: "left", +}; + +const supportSectionStyle: React.CSSProperties = { + textAlign: "center", + padding: "50px 30px", + borderRadius: "20px", + background: "rgba(255, 255, 255, 0.05)", + border: "1px solid rgba(255, 255, 255, 0.1)", + backdropFilter: "blur(10px)", +}; + +const supportTitleStyle: React.CSSProperties = { + fontSize: "1.5rem", + fontWeight: "600", + margin: "0 0 15px 0", + color: "white", +}; + +const supportDescriptionStyle: React.CSSProperties = { + fontSize: "1rem", + color: "rgba(255, 255, 255, 0.8)", + margin: "0 0 30px 0", + lineHeight: 1.5, +}; + +const supportButtonsStyle: React.CSSProperties = { + display: "flex", + gap: "20px", + justifyContent: "center", + flexWrap: "wrap", +}; + +const primarySupportButtonStyle: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: "10px", + padding: "15px 30px", + borderRadius: "12px", + border: "none", + fontSize: "1rem", + fontWeight: "600", + backgroundColor: "#3b82f6", + color: "white", + cursor: "pointer", + transition: "all 0.3s ease", + boxShadow: "0 4px 20px rgba(59, 130, 246, 0.3)", +}; + +const secondarySupportButtonStyle: React.CSSProperties = { + padding: "15px 30px", + borderRadius: "12px", + border: "2px solid rgba(255, 255, 255, 0.3)", + fontSize: "1rem", + fontWeight: "600", + backgroundColor: "transparent", + color: "white", + cursor: "pointer", + transition: "all 0.3s ease", +}; + +const supportButtonIconStyle: React.CSSProperties = { + fontSize: "1.1rem", +}; diff --git a/src/components/organisms/Features.tsx b/src/components/organisms/Features.tsx new file mode 100644 index 0000000..c2503d5 --- /dev/null +++ b/src/components/organisms/Features.tsx @@ -0,0 +1,346 @@ +"use client"; + +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function Features() { + const [isVisible, setIsVisible] = useState(false); + const [activeFeature, setActiveFeature] = useState(null); + + const features = [ + { + id: "real-time-dashboard", + title: "リアルタイム可視化ダッシュボード", + text: "貢献スコア推移、累計報酬、役割ヒートマップ", + icon: "📊", + color: "#ff6b6b", + detailColor: "#ff5252", + }, + { + id: "role-based-permissions", + title: "ロールベース権限管理", + text: "Hats Protocol による可撤回・階層型ロール", + icon: "🎩", + color: "#4ecdc4", + detailColor: "#26c6da", + }, + { + id: "onchain-splits", + title: "完全オンチェーン Splits 配信", + text: "ガス最適化済み、監査・ハイパーストラクチャ", + icon: "💰", + color: "#45b7d1", + detailColor: "#42a5f5", + }, + { + id: "human-readable-addresses", + title: "人に優しいアドレス", + text: "ENS 連携で `community.eth` に直接送金", + icon: "🏷️", + color: "#96c93d", + detailColor: "#8bc34a", + }, + { + id: "api-export", + title: "API & Export", + text: "REST / GraphQL、CSV、Webhook", + icon: "🔗", + color: "#f39c12", + detailColor: "#e67e22", + }, + { + id: "self-custody-mode", + title: "自己責任モード", + text: "カストディアルでないため鍵管理はユーザー責任。利用規約リンクを明記。", + icon: "🔐", + color: "#9b59b6", + detailColor: "#8e44ad", + }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("features"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + return ( +
+
+
+

+ 主な機能ハイライト +

+

プロジェクト運営を革新する6つの核心機能

+
+ +
+ {features.map((feature, index) => ( +
setActiveFeature(index)} + onMouseLeave={() => setActiveFeature(null)} + > +
+
+ {feature.icon} +
+
+ +
+

+ {feature.title} +

+

+ {feature.text} +

+
+ +
+
+
+ + {/* 背景装飾 */} +
+
+
+
+
+ ))} +
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)", + color: "white", + position: "relative", + overflow: "hidden", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", + position: "relative", + zIndex: 1, +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + margin: "0 0 15px 0", + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #ff6b6b, #4ecdc4)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const subtitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "rgba(255, 255, 255, 0.8)", + margin: 0, + fontWeight: "400", +}; + +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))", + gap: "30px", + maxWidth: "1000px", + margin: "0 auto", +}; + +const featureCardStyle: React.CSSProperties = { + padding: "40px 30px", + borderRadius: "24px", + backgroundColor: "white", + border: "1px solid #e2e8f0", + boxShadow: "0 20px 60px rgba(0, 0, 0, 0.15)", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", + cursor: "pointer", + position: "relative", + overflow: "hidden", + opacity: 0, + transform: "translateY(50px)", +}; + +const iconWrapperStyle: React.CSSProperties = { + width: "80px", + height: "80px", + borderRadius: "20px", + border: "2px solid", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: "25px", + marginLeft: "auto", + marginRight: "auto", + position: "relative", +}; + +const iconContainerStyle: React.CSSProperties = { + width: "60px", + height: "60px", + borderRadius: "16px", + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", +}; + +const iconStyle: React.CSSProperties = { + fontSize: "1.8rem", + filter: "drop-shadow(0 2px 4px rgba(255, 255, 255, 0.3))", +}; + +const contentWrapperStyle: React.CSSProperties = { + marginBottom: "25px", +}; + +const featureTitleStyle: React.CSSProperties = { + fontSize: "1.4rem", + fontWeight: "600", + margin: "0 0 15px 0", + lineHeight: 1.3, + transition: "color 0.3s ease", +}; + +const featureTextStyle: React.CSSProperties = { + fontSize: "1rem", + lineHeight: 1.6, + margin: 0, + transition: "color 0.3s ease", +}; + +const progressBarStyle: React.CSSProperties = { + height: "4px", + borderRadius: "2px", + overflow: "hidden", + transition: "background-color 0.3s ease", +}; + +const progressFillStyle: React.CSSProperties = { + height: "100%", + borderRadius: "2px", + transition: "width 0.6s cubic-bezier(0.4, 0, 0.2, 1)", +}; + +const backgroundDecorStyle: React.CSSProperties = { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + pointerEvents: "none", + overflow: "hidden", +}; + +const decorCircle1Style: React.CSSProperties = { + position: "absolute", + top: "-40px", + right: "-40px", + width: "120px", + height: "120px", + borderRadius: "50%", + transition: "transform 0.4s ease", +}; + +const decorCircle2Style: React.CSSProperties = { + position: "absolute", + bottom: "-60px", + left: "-60px", + width: "140px", + height: "140px", + borderRadius: "50%", + transition: "transform 0.4s ease", +}; diff --git a/src/components/organisms/Footer/index.tsx b/src/components/organisms/Footer/index.tsx new file mode 100644 index 0000000..dbdeac0 --- /dev/null +++ b/src/components/organisms/Footer/index.tsx @@ -0,0 +1,527 @@ +"use client"; + +import type React from "react"; +import { useState } from "react"; +import { FaDiscord, FaGithub, FaYoutube } from "react-icons/fa"; +import { FaXTwitter } from "react-icons/fa6"; +import { social, ui } from "../../../themes/settings/color"; + +export default function Footer() { + const [hoveredLink, setHoveredLink] = useState(null); + + const socialLinks = [ + { + id: "github", + name: "GitHub", + icon: , + url: "https://github.com/hackdays-io/toban-lp", + color: social.GitHub, + }, + { + id: "discord", + name: "Discord", + icon: , + url: "https://discord.com/channels/979969380802777169/1277777126359302220", + color: social.Discord, + }, + { + id: "twitter", + name: "X (Twitter)", + icon: , + url: "https://x.com/0xtoban", + color: social.Twitter, + }, + { + id: "youtube", + name: "YouTube", + icon: , + url: "https://www.youtube.com/watch?v=jFjxNSHiCBI", + color: social.YouTube, + }, + ]; + + const quickLinks = [ + { + id: "docs", + name: "ドキュメント", + url: "https://hackdays-io.github.io/toban/docs/welcome", + }, + { id: "api", name: "API リファレンス", url: "#" }, + { id: "tutorial", name: "チュートリアル", url: "#" }, + { id: "community", name: "コミュニティ", url: "#" }, + ]; + + const legalLinks = [ + { id: "company", name: "会社情報", url: "#" }, + { id: "terms", name: "利用規約", url: "#" }, + { id: "privacy", name: "プライバシーポリシー", url: "#" }, + { id: "security", name: "セキュリティ", url: "#" }, + ]; + + const contactInfo = [ + { + id: "support", + label: "サポート", + value: "support@toban.community", + icon: "📧", + }, + { + id: "partnership", + label: "パートナーシップ", + value: "partnership@toban.community", + icon: "🤝", + }, + { + id: "contact-form", + label: "お問い合わせフォーム", + value: + "https://docs.google.com/forms/d/e/1FAIpQLScpzZMaFy9kKN-oibPM2zM154-YtP1v82v1Rf9oARjOz2r8gg/viewform", + icon: "📝", + }, + ]; + + return ( + + ); +} + +const footerStyle: React.CSSProperties = { + background: "linear-gradient(135deg, #FEFBF7 0%, #FFF8F6 50%, #E5DBDF 100%)", + color: "#0E052E", + marginTop: "auto", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + padding: "0 20px", +}; + +const mainContentStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", + gap: "50px", + padding: "80px 0 60px", +}; + +const brandSectionStyle: React.CSSProperties = { + maxWidth: "400px", +}; + +const logoSectionStyle: React.CSSProperties = { + marginBottom: "25px", +}; + +const logoStyle: React.CSSProperties = { + fontSize: "2rem", + fontWeight: "700", + margin: "0 0 10px 0", + background: `linear-gradient(120deg, ${ui.Blue}, ${ui.Green})`, + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const taglineStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "rgba(14, 5, 46, 0.9)", + margin: 0, + fontWeight: "500", + lineHeight: 1.4, +}; + +const descriptionStyle: React.CSSProperties = { + fontSize: "1rem", + color: "rgba(14, 5, 46, 0.7)", + lineHeight: 1.6, + margin: "0 0 30px 0", +}; + +const socialSectionStyle: React.CSSProperties = { + marginTop: "30px", +}; + +const sectionTitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + fontWeight: "600", + color: "rgba(14, 5, 46, 0.9)", + margin: "0 0 20px 0", + letterSpacing: "0.05em", +}; + +const socialLinksStyle: React.CSSProperties = { + display: "flex", + gap: "15px", + flexWrap: "wrap", +}; + +const socialLinkStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "10px 16px", + borderRadius: "12px", + textDecoration: "none", + transition: "all 0.3s ease", + border: "1px solid rgba(14, 5, 46, 0.1)", +}; + +const socialIconStyle: React.CSSProperties = { + fontSize: "1.2rem", +}; + +const socialNameStyle: React.CSSProperties = { + fontSize: "0.9rem", + fontWeight: "500", + color: "rgba(14, 5, 46, 0.8)", +}; + +const linkSectionStyle: React.CSSProperties = { + minWidth: "200px", +}; + +const linkListStyle: React.CSSProperties = { + listStyle: "none", + padding: 0, + margin: 0, +}; + +const linkStyle: React.CSSProperties = { + display: "block", + padding: "8px 0", + textDecoration: "none", + fontSize: "1rem", + transition: "color 0.3s ease", + borderBottom: "1px solid transparent", + color: "rgba(14, 5, 46, 0.8)", +}; + +const contactSectionStyle: React.CSSProperties = { + minWidth: "280px", +}; + +const contactListStyle: React.CSSProperties = { + marginBottom: "30px", +}; + +const contactItemStyle: React.CSSProperties = { + display: "flex", + alignItems: "flex-start", + gap: "12px", + marginBottom: "20px", +}; + +const contactIconStyle: React.CSSProperties = { + fontSize: "1.2rem", + marginTop: "2px", +}; + +const contactLabelStyle: React.CSSProperties = { + fontSize: "0.9rem", + color: "rgba(14, 5, 46, 0.6)", + marginBottom: "4px", + fontWeight: "500", +}; + +const contactValueStyle: React.CSSProperties = { + fontSize: "1rem", + textDecoration: "none", + transition: "color 0.3s ease", + color: "rgba(14, 5, 46, 0.8)", +}; + +const newsletterStyle: React.CSSProperties = { + padding: "25px", + borderRadius: "16px", + background: "rgba(14, 5, 46, 0.05)", + border: "1px solid rgba(14, 5, 46, 0.1)", +}; + +const newsletterTitleStyle: React.CSSProperties = { + fontSize: "1rem", + fontWeight: "600", + color: "rgba(14, 5, 46, 0.9)", + margin: "0 0 15px 0", +}; + +const newsletterFormStyle: React.CSSProperties = { + display: "flex", + gap: "10px", +}; + +const emailInputStyle: React.CSSProperties = { + flex: 1, + padding: "12px 16px", + borderRadius: "8px", + border: "1px solid rgba(14, 5, 46, 0.2)", + background: "rgba(14, 5, 46, 0.1)", + color: "#0E052E", + fontSize: "0.95rem", +}; + +const subscribeButtonStyle: React.CSSProperties = { + padding: "12px 20px", + borderRadius: "8px", + border: "none", + background: ui.Blue, + color: "white", + fontSize: "0.95rem", + fontWeight: "600", + cursor: "pointer", + transition: "all 0.3s ease", +}; + +const bottomBarStyle: React.CSSProperties = { + borderTop: "1px solid rgba(14, 5, 46, 0.1)", + padding: "25px 0", +}; + +const bottomContentStyle: React.CSSProperties = { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + flexWrap: "wrap", + gap: "20px", +}; + +const copyrightStyle: React.CSSProperties = { + flex: 1, +}; + +const copyrightTextStyle: React.CSSProperties = { + fontSize: "0.95rem", + color: "rgba(14, 5, 46, 0.6)", + margin: 0, +}; + +const statusStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", +}; + +const statusItemStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "8px", +}; + +const statusIndicatorStyle: React.CSSProperties = { + width: "8px", + height: "8px", + borderRadius: "50%", + backgroundColor: ui.Green, + animation: "pulse 2s ease-in-out infinite", +}; + +const statusTextStyle: React.CSSProperties = { + fontSize: "0.9rem", + color: "rgba(14, 5, 46, 0.7)", + fontWeight: "500", +}; diff --git a/src/components/organisms/GettingStarted.tsx b/src/components/organisms/GettingStarted.tsx new file mode 100644 index 0000000..98e1b37 --- /dev/null +++ b/src/components/organisms/GettingStarted.tsx @@ -0,0 +1,479 @@ +"use client"; + +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function GettingStarted() { + const [isVisible, setIsVisible] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + + const roles = [ + { + id: "contributor", + role: "コントリビューター", + capabilities: "タスクに応募・履歴を積む", + initialCost: "0 ETH", + icon: "👥", + color: "#3b82f6", + description: + "プロジェクトに参加してタスクを完了し、貢献の履歴を積み重ねていきます。", + benefits: ["履歴の蓄積", "スキルアップ", "コミュニティ参加"], + }, + { + id: "donor", + role: "寄付者", + capabilities: "トークン or ETH をプールへ寄付", + initialCost: "任意", + icon: "💝", + color: "#10b981", + description: + "プロジェクトを資金面で支援し、コミュニティの発展に貢献します。", + benefits: ["社会貢献", "透明性確保", "インパクト追跡"], + }, + { + id: "field-partner", + role: "フィールドパートナー", + capabilities: "地域課題を登録し共同運営", + initialCost: "0 ETH", + icon: "🤝", + color: "#f59e0b", + description: "地域や分野の課題を登録し、プロジェクトを共同で運営します。", + benefits: ["課題解決", "ネットワーク構築", "地域貢献"], + }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.1, rootMargin: "50px" }, + ); + + const element = document.getElementById("getting-started"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + return ( +
+
+
+

+ 参加方法 +
+ Getting Started +

+

+ あなたに最適な参加方法を選んで、今すぐTobanコミュニティに参加しましょう +

+
+ +
+ {roles.map((item, index) => ( +
setSelectedRole(index)} + onMouseLeave={() => setSelectedRole(null)} + > + {/* コストバッジ(右上) */} +
+ {item.initialCost} +
+ + {/* アイコン(中央) */} +
+
+ {item.icon} +
+
+ + {/* メインコンテンツ */} +
+

+ {item.role} +

+ +

{item.capabilities}

+ +

{item.description}

+ + {/* メリット */} +
+

主なメリット

+
    + {item.benefits.map((benefit, benefitIndex) => ( +
  • + + ✓ + + {benefit} +
  • + ))} +
+
+
+ + {/* CTA ボタン */} +
+ +
+
+ ))} +
+ + {/* 全体的なCTA */} +
+
+

始める準備はできましたか?

+

+ 数分で参加できます。まずはコントリビューターとして始めて、 + 徐々に他の役割も体験してみましょう。 +

+ +
+
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)", + minHeight: "100vh", + display: "flex", + alignItems: "center", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + color: "#1e293b", + margin: "0 0 15px 0", + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #3b82f6, #10b981)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const subtitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "#64748b", + margin: 0, + fontWeight: "400", + maxWidth: "600px", + marginLeft: "auto", + marginRight: "auto", +}; + +const rolesGridStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", + gap: "clamp(20px, 4vw, 30px)", + marginBottom: "60px", +}; + +const roleCardStyle: React.CSSProperties = { + padding: "clamp(20px, 4vw, 30px)", + borderRadius: "20px", + backgroundColor: "white", + border: "2px solid", + boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", + cursor: "pointer", + opacity: 0, + transform: "translateY(20px)", + position: "relative", + overflow: "hidden", +}; + +const cardHeaderStyle: React.CSSProperties = { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "25px", +}; + +const iconContainerStyle: React.CSSProperties = { + width: "60px", + height: "60px", + borderRadius: "16px", + border: "2px solid", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginLeft: "auto", + marginRight: "auto", +}; + +const iconStyle: React.CSSProperties = { + fontSize: "1.8rem", +}; + +const costBadgeStyle: React.CSSProperties = { + padding: "8px 16px", + borderRadius: "20px", + fontSize: "0.9rem", + fontWeight: "700", + textTransform: "uppercase", + letterSpacing: "0.05em", +}; + +const cardContentStyle: React.CSSProperties = { + marginBottom: "30px", +}; + +const roleTitleStyle: React.CSSProperties = { + fontSize: "1.4rem", + fontWeight: "600", + margin: "0 0 15px 0", + lineHeight: 1.3, + transition: "color 0.3s ease", +}; + +const roleCapabilitiesStyle: React.CSSProperties = { + fontSize: "1rem", + color: "#64748b", + margin: "0 0 15px 0", + fontWeight: "500", +}; + +const roleDescriptionStyle: React.CSSProperties = { + fontSize: "0.95rem", + color: "#64748b", + margin: "0 0 20px 0", + lineHeight: 1.5, +}; + +const benefitsStyle: React.CSSProperties = { + marginTop: "20px", +}; + +const benefitsTitleStyle: React.CSSProperties = { + fontSize: "1rem", + fontWeight: "600", + color: "#374151", + margin: "0 0 10px 0", +}; + +const benefitsListStyle: React.CSSProperties = { + listStyle: "none", + padding: 0, + margin: 0, +}; + +const benefitItemStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "10px", + fontSize: "0.9rem", + color: "#64748b", + marginBottom: "8px", +}; + +const benefitIconStyle: React.CSSProperties = { + fontSize: "0.8rem", + fontWeight: "bold", +}; + +const cardFooterStyle: React.CSSProperties = { + paddingTop: "20px", + borderTop: "1px solid #f1f5f9", +}; + +const ctaButtonStyle: React.CSSProperties = { + width: "100%", + padding: "15px 25px", + borderRadius: "12px", + border: "2px solid", + fontSize: "1rem", + fontWeight: "600", + cursor: "pointer", + transition: "all 0.3s ease", + backgroundColor: "transparent", +}; + +const mainCtaStyle: React.CSSProperties = { + textAlign: "center", + padding: "clamp(40px, 8vw, 60px) clamp(20px, 6vw, 40px)", + borderRadius: "24px", + background: "linear-gradient(135deg, #3b82f6, #1d4ed8)", + color: "white", + position: "relative", + overflow: "hidden", +}; + +const ctaContentStyle: React.CSSProperties = { + position: "relative", + zIndex: 1, +}; + +const ctaTitleStyle: React.CSSProperties = { + fontSize: "clamp(1.8rem, 3vw, 2.2rem)", + fontWeight: "700", + margin: "0 0 15px 0", + lineHeight: 1.2, +}; + +const ctaDescriptionStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "rgba(255, 255, 255, 0.9)", + margin: "0 0 30px 0", + maxWidth: "600px", + marginLeft: "auto", + marginRight: "auto", + lineHeight: 1.5, +}; + +const ctaButtonsStyle: React.CSSProperties = { + display: "flex", + gap: "clamp(15px, 3vw, 20px)", + justifyContent: "center", + flexWrap: "wrap", +}; + +const primaryCtaButtonStyle: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: "10px", + padding: "18px 36px", + borderRadius: "12px", + border: "none", + fontSize: "1.1rem", + fontWeight: "700", + backgroundColor: "white", + color: "#3b82f6", + cursor: "pointer", + transition: "all 0.3s ease", + boxShadow: "0 4px 20px rgba(0, 0, 0, 0.1)", +}; + +const secondaryCtaButtonStyle: React.CSSProperties = { + padding: "18px 36px", + borderRadius: "12px", + border: "2px solid rgba(255, 255, 255, 0.3)", + fontSize: "1.1rem", + fontWeight: "600", + backgroundColor: "transparent", + color: "white", + cursor: "pointer", + transition: "all 0.3s ease", +}; + +const ctaButtonIconStyle: React.CSSProperties = { + fontSize: "1.2rem", +}; diff --git a/src/components/organisms/Header/index.tsx b/src/components/organisms/Header/index.tsx new file mode 100644 index 0000000..7cf85bb --- /dev/null +++ b/src/components/organisms/Header/index.tsx @@ -0,0 +1,373 @@ +import Button from "@/components/common/Button"; +import { mq } from "@/themes/settings/breakpoints"; +import { brand, neutral } from "@/themes/settings/color"; +import type { ComponentProps } from "@/types"; +import { css } from "@emotion/react"; +import Image from "next/image"; +import Link from "next/link"; +import { type FC, useCallback, useEffect, useRef, useState } from "react"; + +const Header: FC = ({ children }) => { + const [isScrolled, setIsScrolled] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const drawerRef = useRef(null); + const toggleMenu = () => setIsMenuOpen((prev) => !prev); + const closeMenu = useCallback(() => { + setIsMenuOpen(false); + }, []); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 50); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + // ESCで閉じる + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") closeMenu(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [closeMenu]); + + // フォーカスをドロワーに移す(簡易版) + useEffect(() => { + if (isMenuOpen && drawerRef.current) { + drawerRef.current.focus(); + } + }, [isMenuOpen]); + + const headerStyle = css` + backdrop-filter: ${isScrolled ? "blur(8px)" : "none"}; + background-color: ${isScrolled ? "rgba(255, 255, 255, 0.9)" : "transparent"}; + box-shadow: ${isScrolled ? "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)" : "none"}; + left: 0; + position: fixed; + top: 0; + transition: all 0.3s ease; + width: 100%; + z-index: 50; + `; + + const containerStyle = css` + margin: 0 auto; + max-width: 1280px; + padding: 0 1rem; + + ${mq.tablet} { + padding: 0 1.5rem; + } + + ${mq.laptop} { + padding: 0 2rem; + } + `; + + const navContentStyle = css` + align-items: center; + display: flex; + height: 4rem; + justify-content: space-between; + `; + + const logoStyle = css` + align-items: center; + color: ${brand.Secondary}; + display: none; + font-size: 1.5rem; + font-weight: 700; + gap: 0.5rem; + + ${mq.tablet} { + display: flex; + } + `; + + const logoImageStyle = css` + height: 2rem; + width: auto; + `; + + const navLinksStyle = css` + display: none; + + ${mq.tablet} { + align-items: center; + display: flex; + gap: 2rem; + } + `; + + const navLinkStyle = css` + all: unset; + color: ${brand.Secondary}; + cursor: pointer; + display: block; + font-weight: bold; + padding: 0.75rem 1rem; + text-align: center; + + &:hover { + background-color: #eee; + } + `; + + const hamburgerStyle = css` + cursor: pointer; + display: flex; + flex-direction: column; + gap: 5px; + + span { + background-color: ${brand.Secondary}; + height: 3px; + transition: 0.3s; + width: 25px; + } + + ${mq.tablet} { + display: none; + } + `; + + const drawerOverlayStyle = css` + background-color: rgba(0, 0, 0, 0.4); + inset: 0; + opacity: ${isMenuOpen ? 1 : 0}; + pointer-events: ${isMenuOpen ? "auto" : "none"}; + position: fixed; + transition: opacity 0.3s ease; + z-index: 40; + `; + + const drawerMenuStyle = css` + background-color: ${neutral.White}; + display: flex; + flex-direction: column; + gap: 1.5rem; + height: 100vh; + outline: none; + overflow-y: auto; + padding: 2rem 1.5rem 1.5rem; + position: fixed; + right: 0; + top: 0; + transform: ${isMenuOpen ? "translateX(0)" : "translateX(100%)"}; + transition: transform 0.3s ease; + width: 280px; + z-index: 50; + `; + + const closeButtonStyle = css` + align-self: flex-end; + background: none; + border: none; + color: ${brand.Secondary}; + cursor: pointer; + font-size: 1.5rem; + + &:hover { + color: ${brand.Accent1}; + } + `; + + return ( +
+
+
+ +
+ Toban Logo + Toban +
+ + {/* ハンバーガーアイコン */} +
{ + if (e.key === "Enter" || e.key === " ") toggleMenu(); + }} + > + + + +
+ {/* */} +
+
+ {/* ドロワー背景 */} +
{ + if (e.key === "Enter" || e.key === " ") closeMenu(); + }} + /> + {/* ドロワーメニュー */} +
+ + + + + + + + + + + + +
+
+ ); +}; + +export default Header; diff --git a/src/components/organisms/HeroSection.tsx b/src/components/organisms/HeroSection.tsx new file mode 100644 index 0000000..6135d67 --- /dev/null +++ b/src/components/organisms/HeroSection.tsx @@ -0,0 +1,26 @@ +export default function HeroSection() { + return ( + + ); +} diff --git a/src/components/organisms/HowItWorks.tsx b/src/components/organisms/HowItWorks.tsx new file mode 100644 index 0000000..5b5182a --- /dev/null +++ b/src/components/organisms/HowItWorks.tsx @@ -0,0 +1,634 @@ +"use client"; + +import React from "react"; +import { useEffect, useState } from "react"; + +export default function HowItWorks() { + const [isVisible, setIsVisible] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [isMobile, setIsMobile] = useState(false); + const [modalImage, setModalImage] = useState(null); + + const steps = [ + { + id: "signin-join", + number: "01", + title: "サインイン、ワークスペース参加", + description: + "ウォレット、ガス代なしで利用開始\n誰かから役割かアシストクレジットをもらうと自動的に参加", + image: "/assets/HowItWorksImage/HowItWorks1.png", + color: "#ff6b6b", + }, + { + id: "role-check", + number: "02", + title: "役割の確認", + description: + "やるべきことや権限・権利を確認\n他にだれがどんな役割をもっているか見える", + image: "/assets/HowItWorksImage/HowItWorks2.png", + color: "#4ecdc4", + }, + { + id: "assist-credit", + number: "03", + title: "アシストクレジットのやりとり", + description: "助けてもらったとき\n気づきをもらったとき\nただ渡したいとき", + images: [ + "/assets/HowItWorksImage/HowItWorks3-1.png", + "/assets/HowItWorksImage/HowItWorks3-2.png", + ], + color: "#45b7d1", + }, + { + id: "distribution-contract", + number: "04", + title: "分配コントラクトの作成", + description: + "報酬分配をしたい役割を選択\n報酬分配率はアルゴリズムで自動計算", + image: "/assets/HowItWorksImage/HowItWorks4.png", + color: "#96c93d", + }, + { + id: "reward-distribution", + number: "05", + title: "報酬の分配", + description: + "ERC20、ネイティブトークンを分配コントラクトに送金\n1回のトランザクションで一気に分配", + image: "/assets/HowItWorksImage/HowItWorks5.png", + color: "#f39c12", + }, + ]; + + useEffect(() => { + const checkMobile = () => { + const mobile = window.innerWidth <= 768; + setIsMobile(mobile); + // モバイル切り替え時にアニメーションをリセット + if (mobile !== isMobile) { + setIsVisible(false); + setTimeout(() => { + const element = document.getElementById("how-it-works"); + if (element) { + const rect = element.getBoundingClientRect(); + const windowHeight = window.innerHeight; + if (rect.top < windowHeight && rect.bottom > 0) { + setIsVisible(true); + } + } + }, 100); + } + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + return () => window.removeEventListener("resize", checkMobile); + }, [isMobile]); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { + threshold: isMobile ? 0.05 : 0.1, + rootMargin: isMobile ? "100px" : "50px", + }, + ); + + const element = document.getElementById("how-it-works"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, [isMobile]); + + useEffect(() => { + if (isVisible && !isMobile) { + const interval = setInterval(() => { + setCurrentStep((prev) => (prev + 1) % steps.length); + }, 3000); + return () => clearInterval(interval); + } + }, [isVisible, isMobile]); + + return ( +
+
+
+

+ 導入も、運用も、 +
+ シンプルです +

+
+ + {/* ステップ表示 */} +
+ {steps.map((step, index) => ( + + {/* カード */} +
+
+ {/* 画像セクション */} +
+ {step.image ? ( + {step.title} setModalImage(step.image)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setModalImage(step.image); + } + }} + style={{ + maxWidth: "100%", + maxHeight: isMobile ? "200px" : "250px", + objectFit: "contain", + borderRadius: "10px", + boxShadow: "0 4px 15px rgba(0, 0, 0, 0.15)", + cursor: "pointer", + transition: + "transform 0.2s ease, box-shadow 0.2s ease", + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = "scale(1.02)"; + e.currentTarget.style.boxShadow = + "0 6px 20px rgba(0, 0, 0, 0.2)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = "scale(1)"; + e.currentTarget.style.boxShadow = + "0 4px 15px rgba(0, 0, 0, 0.15)"; + }} + /> + ) : step.images ? ( +
+ {step.images.map((img: string, imgIndex: number) => ( + {`${step.title} setModalImage(img)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setModalImage(img); + } + }} + style={{ + maxWidth: isMobile ? "45%" : "180px", + maxHeight: isMobile ? "150px" : "200px", + objectFit: "contain", + borderRadius: "8px", + boxShadow: "0 4px 15px rgba(0, 0, 0, 0.15)", + cursor: "pointer", + transition: + "transform 0.2s ease, box-shadow 0.2s ease", + }} + onMouseEnter={(e) => { + e.currentTarget.style.transform = "scale(1.02)"; + e.currentTarget.style.boxShadow = + "0 6px 20px rgba(0, 0, 0, 0.2)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = "scale(1)"; + e.currentTarget.style.boxShadow = + "0 4px 15px rgba(0, 0, 0, 0.15)"; + }} + /> + ))} +
+ ) : null} +
+ + {/* ステップ番号 */} +
+ {step.number} +
+ + {/* コンテンツセクション */} +
+

+ {step.title} +

+ +

+ {step.description} +

+
+
+ + {/* デスクトップ用矢印(最後のステップ以外) */} + {index < steps.length - 1 && !isMobile && ( +
+
+ → +
+
+ )} +
+ + {/* モバイル用矢印(カードの外側、独立したブロック) */} + {index < steps.length - 1 && isMobile && ( +
+ + ↓ + +
+ )} +
+ ))} +
+ + {/* プログレスバー(デスクトップのみ) */} + {!isMobile && ( +
+
+
+
+
+ Step {currentStep + 1} of {steps.length} +
+
+ )} + + {/* 画像拡大モーダル */} + {modalImage && ( + setModalImage(null)} + onKeyDown={(e) => { + if (e.key === "Escape") { + setModalImage(null); + } + }} + > +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + + 拡大表示 +
+
+ )} +
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + color: "white", + overflow: "hidden", + minHeight: "100vh", + position: "relative", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + margin: 0, + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #ff6b6b, #4ecdc4)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const stepsStyle: React.CSSProperties = { + alignItems: "flex-start", +}; + +const stepCardStyle: React.CSSProperties = { + padding: "30px 20px", + borderRadius: "20px", + backgroundColor: "white", + border: "2px solid #e2e8f0", + textAlign: "center", + position: "relative", + minHeight: "500px", + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "flex-start", + boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", + transition: "all 0.3s ease", +}; + +const stepIconContainerStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + alignItems: "center", +}; + +const stepIconStyle: React.CSSProperties = { + width: "60px", + height: "60px", + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "transform 0.3s ease", +}; + +const iconTextStyle: React.CSSProperties = { + fontSize: "1.8rem", + filter: "drop-shadow(0 2px 4px rgba(255, 255, 255, 0.3))", +}; + +const stepNumberStyle: React.CSSProperties = { + fontSize: "0.9rem", + fontWeight: "700", + color: "#64748b", + letterSpacing: "0.1em", +}; + +const stepTitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + fontWeight: "600", + margin: "0 0 10px 0", + lineHeight: 1.3, +}; + +const stepDescriptionStyle: React.CSSProperties = { + fontSize: "0.9rem", + color: "#64748b", + margin: 0, + lineHeight: 1.4, +}; + +const arrowStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "40px", + position: "absolute", + right: "-30px", + top: "50%", + transform: "translateY(-50%)", + zIndex: 1, +}; + +const arrowIconStyle: React.CSSProperties = { + fontSize: "1.5rem", + color: "rgba(255, 255, 255, 0.8)", + fontWeight: "bold", +}; + +const progressContainerStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "15px", +}; + +const progressBarStyle: React.CSSProperties = { + width: "300px", + height: "6px", + backgroundColor: "rgba(255, 255, 255, 0.2)", + borderRadius: "3px", + overflow: "hidden", +}; + +const progressFillStyle: React.CSSProperties = { + height: "100%", + background: "linear-gradient(90deg, #ff6b6b, #4ecdc4)", + borderRadius: "3px", + transition: "width 0.5s ease", +}; + +const progressTextStyle: React.CSSProperties = { + fontSize: "0.9rem", + color: "rgba(255, 255, 255, 0.8)", + fontWeight: "500", +}; + +const modalOverlayStyle: React.CSSProperties = { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.8)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, + padding: "20px", +}; + +const modalContentStyle: React.CSSProperties = { + position: "relative", + maxWidth: "90vw", + maxHeight: "90vh", + backgroundColor: "white", + borderRadius: "15px", + padding: "20px", + boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)", + display: "flex", + alignItems: "center", + justifyContent: "center", +}; + +const modalCloseButtonStyle: React.CSSProperties = { + position: "absolute", + top: "10px", + right: "15px", + background: "none", + border: "none", + fontSize: "2rem", + cursor: "pointer", + color: "#666", + zIndex: 1001, + width: "40px", + height: "40px", + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "all 0.2s ease", +}; + +const modalImageStyle: React.CSSProperties = { + maxWidth: "100%", + maxHeight: "80vh", + objectFit: "contain", + borderRadius: "10px", +}; diff --git a/src/components/organisms/InfiniteLoop.tsx b/src/components/organisms/InfiniteLoop.tsx new file mode 100644 index 0000000..d911359 --- /dev/null +++ b/src/components/organisms/InfiniteLoop.tsx @@ -0,0 +1,333 @@ +import type React from "react"; +import { useState } from "react"; + +interface LogoItem { + name: string; + logo?: string; +} + +const InfiniteLoop: React.FC = () => { + const [currentSpeed, setCurrentSpeed] = useState(12); + const [isPaused, setIsPaused] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + const logoItems: LogoItem[] = [ + { name: "Comoris" }, + { name: "塩尻DAO" }, + { name: "ento" }, + { name: "ETH Tokyo" }, + { name: "Fracton" }, + { name: "Localcoop" }, + ]; + + const handleSpeedChange = (speed: number) => { + setCurrentSpeed(speed); + }; + + const togglePause = () => { + setIsPaused(!isPaused); + }; + + const containerClasses = ` + infinite-loop-container + ${isPaused || isHovered ? "paused" : ""} + `.trim(); + + return ( +
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+
+ {/* 第1セット */} + {logoItems.map((item) => ( +
+ {item.name} +
+ ))} + + {/* 第2セット(シームレスなループ用) */} + {logoItems.map((item) => ( +
+ {item.name} +
+ ))} +
+
+
+ + +
+ ); +}; + +export default InfiniteLoop; diff --git a/src/components/organisms/Pricing.tsx b/src/components/organisms/Pricing.tsx new file mode 100644 index 0000000..94e0d1c --- /dev/null +++ b/src/components/organisms/Pricing.tsx @@ -0,0 +1,33 @@ +export default function Pricing() { + return ( +
+

はじめやすく、続けやすい。

+ + + + + + + + + + + + + + + + + + + + + + + + + +
プラン内容料金
フリープラン1プロジェクト / 5ロールまで無料
スタンダードAPI連携、人数制限解除月額 ¥4,980〜
カスタム法人・自治体向け支援お問合せ
+
+ ); +} diff --git a/src/components/organisms/ProblemSolution.tsx b/src/components/organisms/ProblemSolution.tsx new file mode 100644 index 0000000..5caa21a --- /dev/null +++ b/src/components/organisms/ProblemSolution.tsx @@ -0,0 +1,369 @@ +"use client"; + +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function ProblemSolution() { + const [isVisible, setIsVisible] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth <= 768); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + return () => window.removeEventListener("resize", checkMobile); + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("problem-solution"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + const problems = [ + { id: "visibility", icon: "📊", text: "貢献の見える化が難しい" }, + { id: "calculation", icon: "🧮", text: "報酬配分の計算が煩雑" }, + { id: "sustainability", icon: "🔄", text: "ボランティアが続かない" }, + { id: "communication", icon: "💬", text: "活動説明が難しい" }, + ]; + + const solutions = [ + { id: "instant-record", icon: "⚡", text: "アシストクレジットで即時記録" }, + { id: "auto-distribution", icon: "🤖", text: "一度設定すれば自動で分配" }, + { id: "token-reward", icon: "🎯", text: "貢献がトークンに" }, + { id: "activity-history", icon: "📈", text: "活動履歴として可視化" }, + ]; + + return ( +
+
+
+

+ 協働プロジェクトの現場の +
+ 「よくある困りごと」 +
+ Tobanが全て解決します +

+
+ +
+ {/* 課題セクション */} +
+
+
⚠️
+

+ 課題 +

+
+
+ {problems.map((item, index) => ( +
+ + {item.icon} + + {item.text} +
+ ))} +
+
+ + {/* 矢印 */} +
+
+ → +
+
+ + {/* モバイル用矢印 */} + {isMobile && ( +
+
+ ↓ +
+
+ )} + + {/* 解決策セクション */} +
+
+
+

+ 解決策 +

+
+
+ {solutions.map((item, index) => ( +
+ + {item.icon} + + {item.text} +
+ ))} +
+
+
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)", + minHeight: "100vh", + display: "flex", + alignItems: "center", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(1.8rem, 4vw, 2.5rem)", + fontWeight: "700", + color: "#1e293b", + lineHeight: 1.3, + margin: 0, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #ff6b6b, #4ecdc4)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const contentGridStyle: React.CSSProperties = { + alignItems: "stretch", +}; + +const cardStyle: React.CSSProperties = { + borderRadius: "20px", + backgroundColor: "white", + boxShadow: "0 20px 60px rgba(0, 0, 0, 0.1)", + border: "1px solid rgba(255, 255, 255, 0.2)", + backdropFilter: "blur(10px)", + opacity: 0, + transform: "translateY(30px)", +}; + +const animationStyle: React.CSSProperties = { + animation: "fadeInUp 0.8s ease-out forwards", +}; + +const cardHeaderStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + marginBottom: "30px", + paddingBottom: "20px", + borderBottom: "2px solid #f1f5f9", +}; + +const problemIconStyle: React.CSSProperties = { + fontSize: "2rem", + marginRight: "15px", + filter: "drop-shadow(0 2px 4px rgba(255, 107, 107, 0.3))", +}; + +const solutionIconStyle: React.CSSProperties = { + fontSize: "2rem", + marginRight: "15px", + filter: "drop-shadow(0 2px 4px rgba(78, 205, 196, 0.3))", +}; + +const cardTitleStyle: React.CSSProperties = { + fontSize: "1.5rem", + fontWeight: "600", + margin: 0, + color: "#1e293b", +}; + +const listContainerStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + gap: "20px", +}; + +const listItemStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + padding: "15px 20px", + backgroundColor: "rgba(248, 250, 252, 0.8)", + borderRadius: "12px", + border: "1px solid #e2e8f0", + transition: "all 0.3s ease", + cursor: "pointer", + opacity: 0, + transform: "translateX(-20px)", +}; + +const listItemAnimationStyle: React.CSSProperties = { + animation: "slideInLeft 0.6s ease-out forwards", +}; + +const iconStyle: React.CSSProperties = { + fontSize: "1.5rem", + marginRight: "15px", + minWidth: "30px", +}; + +const arrowContainerStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + justifyContent: "center", +}; + +const arrowStyle: React.CSSProperties = { + fontSize: "3rem", + color: "#64748b", + fontWeight: "bold", + opacity: 0, + transform: "scale(0.5)", +}; + +const arrowAnimationStyle: React.CSSProperties = { + animation: "bounceIn 1s ease-out 0.5s forwards", +}; diff --git a/src/components/organisms/SecurityStack.tsx b/src/components/organisms/SecurityStack.tsx new file mode 100644 index 0000000..f9611a1 --- /dev/null +++ b/src/components/organisms/SecurityStack.tsx @@ -0,0 +1,437 @@ +"use client"; + +import type React from "react"; +import { useEffect, useState } from "react"; + +export default function SecurityStack() { + const [isVisible, setIsVisible] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [hoveredCard, setHoveredCard] = useState(null); + + const protocols = [ + { + name: "Hats Protocol", + logo: "/assets/logo/hats-logo.png", + role: "権限付与・剥奪を NFT 化", + trust: "非転送型 ERC-1155。ロールは DAO が管理", + link: "https://docs.hatsprotocol.xyz", + linkText: "docs.hatsprotocol.xyz", + color: "#ff6b6b", + icon: "🎩", + }, + { + name: "0xSplits", + logo: "/assets/logo/splits-logo.png", + role: "報酬の自動分配", + trust: "監査済み & ハイパーストラクチャ", + link: "https://review.mirror.xyz", + linkText: "review.mirror.xyz", + color: "#4ecdc4", + icon: "💰", + }, + { + name: "ENS", + logo: "/assets/logo/ens-mark-Blue.svg", + role: "認識しやすい受取アドレス", + trust: "オープンソース/分散管理", + link: "https://ens.domains", + linkText: "ens.domains", + color: "#45b7d1", + icon: "🌐", + }, + ]; + + const securityFeatures = [ + { icon: "🔓", text: "すべてオープンソース", color: "#ff6b6b" }, + { + icon: "🛡️", + text: "スマートコントラクトはすべて監査済み", + color: "#4ecdc4", + }, + { icon: "⚔️", text: "複数コミュニティで実戦投入", color: "#45b7d1" }, + ]; + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth <= 768); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + return () => window.removeEventListener("resize", checkMobile); + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("security-stack"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + return ( +
+
+ {/* ヘッダー */} +
+

+ 🔒 + セキュリティ &
+ プロトコルスタック +

+
+ + {/* セキュリティ機能 */} +
+ {securityFeatures.map((feature, index) => ( +
+ + {feature.icon} + + + {feature.text} + +
+ ))} +
+ + {/* プロトコルカード */} +
+ {protocols.map((protocol, index) => ( +
!isMobile && setHoveredCard(index)} + onMouseLeave={() => !isMobile && setHoveredCard(null)} + > + {/* ロゴとタイトル */} +
+
+ {protocol.name} +
+

+ {protocol.name} +

+
+ + {/* 役割 */} +
+
+ Toban での役割 +
+

+ {protocol.role} +

+
+ + {/* 信頼ポイント */} +
+
+ 信頼ポイント +
+

+ {protocol.trust}{" "} + + {protocol.linkText} + +

+
+
+ ))} +
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #1e293b 0%, #334155 100%)", + color: "white", + position: "relative", + overflow: "hidden", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", + position: "relative", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + margin: 0, + lineHeight: 1.2, +}; + +const securityIconStyle: React.CSSProperties = { + display: "inline-block", + marginRight: "15px", + fontSize: "2.5rem", + filter: "drop-shadow(0 4px 8px rgba(255, 215, 0, 0.4))", +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #ffd700, #ff6b6b)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const featuresContainerStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + gap: "15px", + alignItems: "center", +}; + +const featureItemStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "15px", + padding: "20px 25px", + backgroundColor: "rgba(255, 255, 255, 0.1)", + borderRadius: "50px", + backdropFilter: "blur(10px)", + border: "1px solid rgba(255, 255, 255, 0.2)", + opacity: 0, + transform: "translateY(20px)", + maxWidth: "600px", + width: "100%", +}; + +const featureAnimationStyle: React.CSSProperties = { + animation: "slideInRight 0.8s ease-out forwards", +}; + +const featureIconStyle: React.CSSProperties = { + fontSize: "2rem", +}; + +const protocolGridStyle: React.CSSProperties = { + alignItems: "stretch", +}; + +const protocolCardStyle: React.CSSProperties = { + backgroundColor: "white", + borderRadius: "20px", + padding: "35px 30px", + border: "2px solid #e2e8f0", + color: "#1e293b", + boxShadow: "0 20px 60px rgba(0, 0, 0, 0.1)", + cursor: "pointer", + position: "relative", +}; + +const protocolHeaderStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + alignItems: "center", + marginBottom: "30px", +}; + +const logoContainerStyle: React.CSSProperties = { + width: "120px", + height: "120px", + borderRadius: "20px", + backgroundColor: "#f8f9fa", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: "20px", + transition: "all 0.3s ease", +}; + +const logoStyle: React.CSSProperties = { + width: "80px", + height: "auto", + maxHeight: "80px", + objectFit: "contain", +}; + +const protocolTitleStyle: React.CSSProperties = { + fontSize: "1.5rem", + fontWeight: "700", + margin: 0, + transition: "color 0.3s ease", +}; + +const roleContainerStyle: React.CSSProperties = { + marginBottom: "20px", +}; + +const trustContainerStyle: React.CSSProperties = { + marginBottom: 0, +}; + +const labelStyle: React.CSSProperties = { + display: "inline-block", + padding: "6px 12px", + borderRadius: "20px", + fontSize: "0.9rem", + fontWeight: "600", + color: "white", + marginBottom: "10px", +}; + +const roleTextStyle: React.CSSProperties = { + fontSize: "1rem", + lineHeight: 1.6, + margin: 0, + color: "#475569", +}; + +const trustTextStyle: React.CSSProperties = { + fontSize: "1rem", + lineHeight: 1.6, + margin: 0, + color: "#475569", +}; + +const linkStyle: React.CSSProperties = { + textDecoration: "none", + fontWeight: "600", + transition: "opacity 0.3s ease", +}; diff --git a/src/components/organisms/TrackRecord.tsx b/src/components/organisms/TrackRecord.tsx new file mode 100644 index 0000000..4174d92 --- /dev/null +++ b/src/components/organisms/TrackRecord.tsx @@ -0,0 +1,369 @@ +"use client"; + +import type React from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; + +export default function TrackRecord() { + const [isVisible, setIsVisible] = useState(false); + const [animatedNumbers, setAnimatedNumbers] = useState([0, 0, 0]); + const [isMobile, setIsMobile] = useState(true); // 初期値をtrueに設定してSSR時にパルスを無効化 + const intervalRefs = useRef<(NodeJS.Timeout | null)[]>([null, null, null]); + + const records = [ + { + id: "total-value", + metric: "累計流通額", + value: "¥125,000,000 相当", + numericValue: 125000000, + suffix: " 相当", + prefix: "¥", + icon: "💰", + color: "#10b981", + }, + { + id: "organizations", + metric: "導入組織数", + value: "34 DAO / NPO / 地域団体", + numericValue: 34, + suffix: " DAO / NPO / 地域団体", + prefix: "", + icon: "🏢", + color: "#3b82f6", + }, + { + id: "transactions", + metric: "処理 Tx", + value: "18,420 Tx", + numericValue: 18420, + suffix: " Tx", + prefix: "", + icon: "⚡", + color: "#f59e0b", + }, + ]; + + const animateNumber = useCallback( + (finalValue: number, index: number, duration = 2000) => { + let startValue = 0; + const increment = finalValue / (duration / 50); + + const intervalId = intervalRefs.current[index]; + if (intervalId) { + clearInterval(intervalId); + } + + intervalRefs.current[index] = setInterval(() => { + startValue += increment; + if (startValue >= finalValue) { + startValue = finalValue; + const intervalId = intervalRefs.current[index]; + if (intervalId) { + clearInterval(intervalId); + } + intervalRefs.current[index] = null; + } + setAnimatedNumbers((prev) => { + const newNumbers = [...prev]; + newNumbers[index] = Math.floor(startValue); + return newNumbers; + }); + }, 50); + }, + [], + ); + + const formatNumber = (num: number, index: number) => { + const record = records[index]; + if (index === 0) { + // 累計流通額 + return `${record.prefix}${num.toLocaleString()}`; + } + if (index === 2) { + // 処理 Tx + return num.toLocaleString(); + } + return num.toString(); + }; + + useEffect(() => { + // より確実なモバイル判定 + const checkMobile = () => { + const isMobileWidth = window.innerWidth <= 768; + const isMobileUserAgent = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent, + ); + const isTouchDevice = + "ontouchstart" in window || navigator.maxTouchPoints > 0; + + // いずれかの条件が満たされればモバイルとして判定 + setIsMobile(isMobileWidth || isMobileUserAgent || isTouchDevice); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !isVisible) { + setIsVisible(true); + // アニメーション開始(少し遅延を付けて順次実行) + records.forEach((record, index) => { + setTimeout(() => { + animateNumber(record.numericValue, index); + }, index * 300); + }); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("track-record"); + if (element) { + observer.observe(element); + } + + return () => { + window.removeEventListener("resize", checkMobile); + if (element) { + observer.unobserve(element); + } + // クリーンアップ + for (const interval of intervalRefs.current) { + if (interval) clearInterval(interval); + } + }; + }, [animateNumber, isVisible]); // 依存配列を正しく設定 + + return ( +
+
+
+

+ トラックレコード +
+ (2024-06→現在) +

+
+ +
+ {records.map((record, index) => ( +
+
+ {record.icon} +
+ +
+

{record.metric}

+

+ {formatNumber(animatedNumbers[index], index)} + {record.suffix} +

+
+ +
+
+
+ + {/* パルスエフェクト(モバイルでは非表示) */} + {isVisible && !isMobile && ( +
+ )} +
+ ))} +
+ +
+

+ 💡 注:実データは on-chain から自動取得し動的更新に。 +

+
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%)", + color: "white", + position: "relative", + overflow: "hidden", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1200px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.8rem)", + fontWeight: "700", + margin: 0, + lineHeight: 1.2, +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #10b981, #3b82f6)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const periodStyle: React.CSSProperties = { + fontSize: "1.2rem", + fontWeight: "400", + color: "rgba(255, 255, 255, 0.8)", +}; + +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", + gap: "40px", + maxWidth: "1000px", + margin: "0 auto", + marginBottom: "60px", +}; + +const cardStyle: React.CSSProperties = { + padding: "40px", + borderRadius: "24px", + background: "rgba(255, 255, 255, 0.05)", + border: "2px solid", + backdropFilter: "blur(10px)", + boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)", + transition: "all 0.6s cubic-bezier(0.4, 0, 0.2, 1)", + position: "relative", + overflow: "hidden", + opacity: 0, + transform: "translateY(50px) scale(0.9)", +}; + +const iconContainerStyle: React.CSSProperties = { + width: "70px", + height: "70px", + borderRadius: "18px", + border: "2px solid", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: "25px", + marginLeft: "auto", + marginRight: "auto", +}; + +const iconStyle: React.CSSProperties = { + fontSize: "2rem", + filter: "drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))", +}; + +const contentStyle: React.CSSProperties = { + marginBottom: "25px", +}; + +const metricStyle: React.CSSProperties = { + fontSize: "1rem", + color: "rgba(255, 255, 255, 0.8)", + margin: "0 0 15px 0", + fontWeight: "500", + letterSpacing: "0.05em", +}; + +const valueStyle: React.CSSProperties = { + fontSize: "clamp(1.8rem, 4vw, 2.2rem)", + fontWeight: "700", + margin: 0, + lineHeight: 1.2, +}; + +const suffixStyle: React.CSSProperties = { + fontSize: "0.7em", + fontWeight: "400", + opacity: 0.8, +}; + +const progressBarStyle: React.CSSProperties = { + height: "6px", + borderRadius: "3px", + overflow: "hidden", +}; + +const progressFillStyle: React.CSSProperties = { + height: "100%", + borderRadius: "3px", + transition: "width 1s cubic-bezier(0.4, 0, 0.2, 1)", +}; + +const pulseStyle: React.CSSProperties = { + position: "absolute", + top: "50%", + left: "50%", + width: "300px", + height: "300px", + borderRadius: "50%", + transform: "translate(-50%, -50%)", + pointerEvents: "none", + zIndex: -1, +}; + +const noteContainerStyle: React.CSSProperties = { + textAlign: "center", + padding: "30px", + borderRadius: "16px", + background: "rgba(255, 255, 255, 0.05)", + border: "1px solid rgba(255, 255, 255, 0.1)", + backdropFilter: "blur(10px)", +}; + +const noteStyle: React.CSSProperties = { + fontSize: "1rem", + color: "rgba(255, 255, 255, 0.7)", + margin: 0, + fontStyle: "italic", +}; diff --git a/src/components/organisms/UseCases.tsx b/src/components/organisms/UseCases.tsx new file mode 100644 index 0000000..93f15fe --- /dev/null +++ b/src/components/organisms/UseCases.tsx @@ -0,0 +1,579 @@ +"use client"; + +import React, { useState, useEffect } from "react"; + +export default function UseCases() { + const [isVisible, setIsVisible] = useState(false); + const [hoveredCard, setHoveredCard] = useState(null); + const [selectedCard, setSelectedCard] = useState(null); + + const cases = [ + { + id: "municipality", + title: "自治体の課長さんへ", + subtitle: "(地域振興 / 住民参加施策)", + icon: "🏛️", + color: "#3b82f6", + gradient: "linear-gradient(135deg,rgb(95, 140, 212), #1d4ed8)", + challenges: [ + "ボランティア活動の参加率が伸びない", + "「誰がどれくらい活動しているか」を把握できない", + "助成金や成果報告に必要なデータ収集が手間", + ], + solutions: [ + "活動参加をアシストクレジットで即時記録", + "住民の貢献を見える化して、モチベーション向上", + "データが蓄積されるので、助成金申請・行政報告に活用可能", + ], + value: "透明で持続可能な「住民参加型まちづくり」を実現できる", + }, + { + id: "enterprise", + title: "企業の担当者さんへ", + subtitle: "(新規事業 / コミュニティマーケティング)", + icon: "🏢", + color: "#10b981", + gradient: "linear-gradient(135deg, #10b981, #047857)", + challenges: [ + "社内外コミュニティでの貢献が定量化できない", + "コミュニティ施策のROIを上司や経営陣に説明しにくい", + "インセンティブやリワードが不公平になりがち", + ], + solutions: [ + "定量的な貢献ログをダッシュボードで可視化", + "スマートコントラクトで公平な報酬分配", + "API連携で既存マーケティングシステムにデータ統合", + ], + value: + "「成果が見える」コミュニティ施策として社内説明・承認が取りやすくなる", + }, + { + id: "community", + title: "地域コミュニティマネージャーさんへ", + subtitle: "(NPO・団体運営者)", + icon: "🤝", + color: "#f59e0b", + gradient: "linear-gradient(135deg, #f59e0b, #d97706)", + challenges: [ + "会員やボランティアの参加状況が曖昧", + "貢献に対して感謝やリワードをどう設計するか悩む", + "活動の継続性が不安定", + ], + solutions: [ + "役割NFTでメンバーの役割を明確化", + "貢献量に応じた自動報酬で不公平感を減らす", + "活動ログを継続的に蓄積し、信頼の見える化", + ], + value: + "貢献が「忘れられない・不公平にならない」仕組みで、持続的なコミュニティ運営が可能", + }, + { + id: "creator", + title: "コミュニティ創設者さん・\n運営者さんへ", + subtitle: "", + icon: "💻", + color: "#8b5cf6", + gradient: "linear-gradient(135deg,rgb(149, 128, 196), #7c3aed)", + challenges: [ + "仲間の貢献が可視化されにくい", + "金銭的な報酬やインセンティブ設計が難しい", + "運営の負担が集中しやすい", + ], + solutions: [ + "役割NFTとアシストクレジットで「誰がどのくらい貢献したか」を明確化", + "貢献に応じて報酬を自動分配、透明性を確保", + "活動記録を残せるため、次の協力者を巻き込みやすい", + ], + value: + "信頼できる仕組みで、コミュニティをゼロからでも安心して立ち上げられる", + }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.3 }, + ); + + const element = document.getElementById("use-cases"); + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + }; + }, []); + + return ( +
+
+
+

+ あなたの現場でも、 +
+ すぐに役立ちます +

+
+ +
+ {cases.map((item, index) => ( + + ))} +
+
+
+ ); +} + +const sectionStyle: React.CSSProperties = { + padding: "80px 20px", + background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)", + minHeight: "100vh", + display: "flex", + alignItems: "center", +}; + +const containerStyle: React.CSSProperties = { + maxWidth: "1400px", + margin: "0 auto", + width: "100%", +}; + +const headerStyle: React.CSSProperties = { + textAlign: "center", + marginBottom: "60px", +}; + +const titleStyle: React.CSSProperties = { + fontSize: "clamp(2rem, 4vw, 2.5rem)", + fontWeight: "700", + color: "#1e293b", + lineHeight: 1.3, + margin: 0, +}; + +const subtitleStyle: React.CSSProperties = { + fontSize: "1.1rem", + color: "#64748b", + marginTop: "20px", + lineHeight: 1.6, + maxWidth: "600px", + margin: "20px auto 0 auto", +}; + +const highlightStyle: React.CSSProperties = { + background: "linear-gradient(120deg, #3b82f6, #8b5cf6)", + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + backgroundClip: "text", +}; + +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(350px, 1fr))", + gap: "30px", + maxWidth: "1200px", + margin: "0 auto", +}; + +const cardStyle: React.CSSProperties = { + padding: "30px", + borderRadius: "20px", + backgroundColor: "white", + border: "1px solid #e2e8f0", + boxShadow: "0 10px 40px rgba(0, 0, 0, 0.1)", + transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", + cursor: "pointer", + position: "relative", + overflow: "hidden", + opacity: 0, + transform: "translateY(30px)", + display: "flex", + flexDirection: "column", +}; + +const iconContainerStyle: React.CSSProperties = { + width: "70px", + height: "70px", + borderRadius: "16px", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: "20px", + marginLeft: "auto", + marginRight: "auto", + border: "2px solid", + transition: "all 0.3s ease", +}; + +const iconStyle: React.CSSProperties = { + fontSize: "2rem", + filter: "drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1))", +}; + +const contentStyle: React.CSSProperties = { + flex: 1, + marginBottom: "20px", +}; + +const cardTitleStyle: React.CSSProperties = { + fontSize: "1.4rem", + fontWeight: "700", + margin: "0 0 8px 0", + lineHeight: 1.2, + transition: "color 0.3s ease", +}; + +const subtitleCardStyle: React.CSSProperties = { + fontSize: "0.9rem", + margin: "0 0 20px 0", + lineHeight: 1.4, + fontWeight: "500", + transition: "color 0.3s ease", +}; + +const previewStyle: React.CSSProperties = { + marginTop: "15px", +}; + +const previewSectionStyle: React.CSSProperties = { + marginBottom: "15px", +}; + +const sectionTitleStyle: React.CSSProperties = { + fontSize: "1rem", + fontWeight: "600", + margin: "0 0 10px 0", + textTransform: "uppercase", + letterSpacing: "0.5px", + transition: "color 0.3s ease", +}; + +const previewTextStyle: React.CSSProperties = { + fontSize: "0.95rem", + lineHeight: 1.5, + margin: 0, + transition: "color 0.3s ease", +}; + +const detailStyle: React.CSSProperties = { + marginTop: "15px", +}; + +const detailSectionStyle: React.CSSProperties = { + marginBottom: "25px", +}; + +const listStyle: React.CSSProperties = { + margin: "0", + paddingLeft: "0", + listStyle: "none", +}; + +const listItemStyle: React.CSSProperties = { + fontSize: "0.95rem", + lineHeight: 1.6, + marginBottom: "8px", + paddingLeft: "20px", + position: "relative", + transition: "color 0.3s ease", +}; + +const valueContainerStyle: React.CSSProperties = { + padding: "20px", + borderRadius: "12px", + backgroundColor: "rgba(59, 130, 246, 0.05)", + border: "1px solid rgba(59, 130, 246, 0.1)", + marginTop: "20px", +}; + +const valueTextStyle: React.CSSProperties = { + fontSize: "1rem", + fontWeight: "600", + lineHeight: 1.5, + margin: 0, + transition: "color 0.3s ease", +}; + +const cardFooterStyle: React.CSSProperties = { + paddingTop: "20px", + borderTop: "1px solid", + transition: "border-color 0.3s ease", + textAlign: "center", +}; + +const learnMoreStyle: React.CSSProperties = { + fontSize: "0.9rem", + fontWeight: "600", + transition: "color 0.3s ease", +}; + +const decorationStyle: React.CSSProperties = { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + pointerEvents: "none", + overflow: "hidden", +}; + +const decoration1Style: React.CSSProperties = { + position: "absolute", + top: "-50%", + right: "-20%", + width: "100px", + height: "100px", + borderRadius: "50%", + backgroundColor: "rgba(255, 255, 255, 0.1)", + animation: "float 3s ease-in-out infinite", +}; + +const decoration2Style: React.CSSProperties = { + position: "absolute", + bottom: "-30%", + left: "-10%", + width: "60px", + height: "60px", + borderRadius: "50%", + backgroundColor: "rgba(255, 255, 255, 0.1)", + animation: "float 3s ease-in-out infinite reverse", +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 956fa84..1e82c85 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,5 +1,15 @@ import type { AppProps } from "next/app"; +import "../themes/styles/globals.css"; +import Head from "next/head"; export default function App({ Component, pageProps }: AppProps) { - return ; + return ( + <> + + + + + + + ); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 8213990..3697cba 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,25 +1,38 @@ -import { css } from "@emotion/react"; - -const pageStyle = css` - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, sans-serif; -`; - -const titleStyle = css` - font-size: 3rem; - font-weight: bold; - color: #333; - text-align: center; -`; +import Layout from "@/components/layouts/base"; +import AwardsMedia from "../components/organisms/AwardsMedia"; +import CaseStudies from "../components/organisms/CaseStudies"; +import Faq from "../components/organisms/Faq"; +import Features from "../components/organisms/Features"; +import GettingStarted from "../components/organisms/GettingStarted"; +import HeroSection from "../components/organisms/HeroSection"; +import HowItWorks from "../components/organisms/HowItWorks"; +import InfiniteLoop from "../components/organisms/InfiniteLoop"; +import Pricing from "../components/organisms/Pricing"; +import ProblemSolution from "../components/organisms/ProblemSolution"; +import SecurityStack from "../components/organisms/SecurityStack"; +import TrackRecord from "../components/organisms/TrackRecord"; +import UseCases from "../components/organisms/UseCases"; export default function Home() { return ( -
-

Hello, Toban LP!

-
+ +
+
+ + + + + + + + + + + + {/* */} + +
+
+
); } diff --git a/src/themes/global.ts b/src/themes/global.ts new file mode 100644 index 0000000..eb60a8d --- /dev/null +++ b/src/themes/global.ts @@ -0,0 +1,38 @@ +import { css } from "@emotion/react"; +import { mq } from "./settings/breakpoints"; +import { info, neutral } from "./settings/color"; +import {} from "./settings/spaces"; + +export const globalStyles = css({ + "html, body": { + color: neutral.White, + margin: "0", + padding: "0", + }, + + h1: { + fontWeight: 500, + letterSpacing: "0.07rem", + }, + + h2: { + fontWeight: 800, + letterSpacing: "0.1rem", + + [mq.laptop]: { + fontSize: "2rem", + }, + }, + + h3: {}, + + p: { + fontWeight: 300, + lineHeight: 2, + letterSpacing: "0.07rem", + }, + + a: { color: neutral.White, "&:hover": { color: info.Attention } }, + + li: { margin: "12px 0;" }, +}); diff --git a/src/themes/settings/breakpoints.ts b/src/themes/settings/breakpoints.ts new file mode 100644 index 0000000..aea6372 --- /dev/null +++ b/src/themes/settings/breakpoints.ts @@ -0,0 +1,8 @@ +export const mq = { + mobileSmall: "@media (max-width: 479px)", + mobile: "@media (min-width: 480px)", + tablet: "@media (min-width: 768px)", + laptop: "@media (min-width: 1024px)", + desktop: "@media (min-width: 1440px)", + monitor: "@media (min-width: 2560px)", +} as const; diff --git a/src/themes/settings/color.ts b/src/themes/settings/color.ts new file mode 100644 index 0000000..aa1827c --- /dev/null +++ b/src/themes/settings/color.ts @@ -0,0 +1,69 @@ +/* Color Pallet */ + +export const neutral = { + White: "#FFFFFF", + Grey1: "#FFF8F6", + Grey2: "#E5DBDF", + Grey3: "#BFB2C0", + Grey4: "#775566", + Grey5: "#332233", + Grey6: "#221122", + Black: "#000000", + Text: "#111111", +} as const; + +export const brand = { + Primary: "#FEFBF7", + Secondary: "#0E052E", + Accent1: "#EAC435", + Accent2: "#B07C8C", + + // 互換性のために残しておく従来の色名(新しい色で置き換え) + OffWhite: "#FEFBF7", // brand.Primary と同じ + Indigo: "#0E052E", // brand.Secondary と同じ + TurkishRose: "#B07C8C", // brand.Accent2 と同じ + BluePantone: "#4455FF", // themeLight.Link と同じ + MustardYellow: "#EAC435", // brand.Accent1 と同じ +} as const; + +export const themeLight = { + Primary: brand.Primary, + PrimaryHighContrast: "#CC3322", + PrimaryLowContrast: "#FFDDDD", + PrimaryHover: "#FF7766", + PrimaryVisited: "#CC4433", + + Secondary: brand.Secondary, + SecondaryHighContrast: "#331144", + SecondaryLowContrast: "#EEDDFF", + + Link: "#4455FF", + LinkHover: "#DD66FF", + + Body: neutral.Text, + BodyLight: neutral.Grey4, + Disable: neutral.Grey3, + Background: "#FEFBF7", + BackgroundHighlight: neutral.Grey1, +} as const; + +export const info = { + Success: "#668844", + SuccessLight: "#EEFFDD", + Error: brand.Primary, + ErrorLight: "#FFEEEE", + Attention: "#FFAA44", + AttentionLight: "#FFF8DD", +} as const; + +export const social = { + GitHub: "#333", + Discord: "#5865f2", + Twitter: "#1da1f2", + YouTube: "#ff0000", +} as const; + +export const ui = { + Blue: "#60a5fa", + Green: "#34d399", +} as const; diff --git a/src/themes/settings/spaces.ts b/src/themes/settings/spaces.ts new file mode 100644 index 0000000..bb0d46c --- /dev/null +++ b/src/themes/settings/spaces.ts @@ -0,0 +1,102 @@ +// spacing constants +// every increment corresponds to an additional 0.25rem / 4px. + +export const spacing = { + 0: "0px", + 0.25: "0.25rem", + 0.5: "0.5rem", + 0.75: "0.75rem", + 1: "1rem", + 1.25: "1.25rem", + 1.5: "1.5rem", + 1.75: "1.75rem", + 2: "2rem", + 2.25: "2.25rem", + 2.5: "2.5rem", + 2.75: "2.75rem", + 3: "3rem", + 3.25: "3.25rem", + 3.5: "3.5rem", + 3.75: "3.75rem", + 4: "4rem", + 4.25: "4.25rem", + 4.5: "4.5rem", + 4.75: "4.75rem", + 5: "5rem", + 5.25: "5.25rem", + 5.5: "5.5rem", + 5.75: "5.75rem", + 6: "6rem", + 6.25: "6.25rem", + 6.5: "6.5rem", + 6.75: "6.75rem", + 7: "7rem", + 7.25: "7.25rem", + 7.5: "7.5rem", + 7.75: "7.75rem", + 8: "8rem", + 8.25: "8.25rem", + 8.5: "8.5rem", + 8.75: "8.75rem", + 9: "9rem", + 9.25: "9.25rem", + 9.5: "9.5rem", + 9.75: "9.75rem", + 10: "10rem", + 10.25: "10.25rem", + 10.5: "10.5rem", + 10.75: "10.75rem", + 11: "11rem", + 11.25: "11.25rem", + 11.5: "11.5rem", + 11.75: "11.75rem", + 12: "12rem", + 12.25: "12.25rem", + 12.5: "12.5rem", + 12.75: "12.75rem", + 13: "13rem", + 13.25: "13.25rem", + 13.5: "13.5rem", + 13.75: "13.75rem", + 14: "14rem", + 14.25: "14.25rem", + 14.5: "14.5rem", + 14.75: "14.75rem", + 15: "15rem", + 15.25: "15.25rem", + 15.5: "15.5rem", + 15.75: "15.75rem", + 16: "16rem", + 16.25: "16.25rem", + 16.5: "16.5rem", + 16.75: "16.75rem", + 17: "17rem", + 17.25: "17.25rem", + 17.5: "17.5rem", + 17.75: "17.75rem", + 18: "18rem", + 18.25: "18.25rem", + 18.5: "18.5rem", + 18.75: "18.75rem", + 19: "19rem", + 19.25: "19.25rem", + 19.5: "19.5rem", + 19.75: "19.75rem", + 20: "20rem", + 20.25: "20.25rem", + 20.5: "20.5rem", + 20.75: "20.75rem", + 21: "21rem", + 21.25: "21.25rem", + 21.5: "21.5rem", + 21.75: "21.75rem", + 22: "22rem", + 22.25: "22.25rem", + 22.5: "22.5rem", + 22.75: "22.75rem", + 23: "23rem", + 23.25: "23.25rem", + 23.5: "23.5rem", + 23.75: "23.75rem", + 24: "24rem", +}; diff --git a/src/themes/styles/globals.css b/src/themes/styles/globals.css new file mode 100644 index 0000000..92becba --- /dev/null +++ b/src/themes/styles/globals.css @@ -0,0 +1,994 @@ +/* Mobile center fix v2.0 */ +:root { + --background: #ffffff; + --foreground: #171717; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: var(--foreground); + background: var(--background); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin: 0; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +/* 無限ループアニメーション */ +@keyframes scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } +} + +/* モダンコンポーネント用アニメーション */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes bounceIn { + from { + opacity: 0; + transform: scale(0.5); + } + 60% { + transform: scale(1.1); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes zoomIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes countUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes float { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg); + } + 33% { + transform: translate(15px, -15px) rotate(120deg); + } + 66% { + transform: translate(-10px, 10px) rotate(240deg); + } +} + +@keyframes pulse { + 0% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0.8; + } + 50% { + transform: translate(-50%, -50%) scale(1.2); + opacity: 0.3; + } + 100% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0.8; + } +} + +/* ステップスクロールアニメーション */ +@keyframes scrollStep { + 0% { + transform: translateX(0); + } + 8.33% { + transform: translateX(calc(-220px * 1)); + } + 16.66% { + transform: translateX(calc(-220px * 1)); + } + 25% { + transform: translateX(calc(-220px * 2)); + } + 33.33% { + transform: translateX(calc(-220px * 2)); + } + 41.66% { + transform: translateX(calc(-220px * 3)); + } + 50% { + transform: translateX(calc(-220px * 3)); + } + 58.33% { + transform: translateX(calc(-220px * 4)); + } + 66.66% { + transform: translateX(calc(-220px * 4)); + } + 75% { + transform: translateX(calc(-220px * 5)); + } + 83.33% { + transform: translateX(calc(-220px * 5)); + } + 91.66% { + transform: translateX(calc(-220px * 6)); + } + 100% { + transform: translateX(calc(-220px * 6)); + } +} + +/* レスポンシブ対応のステップスクロール */ +@media (max-width: 768px) { + @keyframes scrollStep { + 0% { + transform: translateX(0); + } + 8.33% { + transform: translateX(calc(-160px * 1)); + } + 16.66% { + transform: translateX(calc(-160px * 1)); + } + 25% { + transform: translateX(calc(-160px * 2)); + } + 33.33% { + transform: translateX(calc(-160px * 2)); + } + 41.66% { + transform: translateX(calc(-160px * 3)); + } + 50% { + transform: translateX(calc(-160px * 3)); + } + 58.33% { + transform: translateX(calc(-160px * 4)); + } + 66.66% { + transform: translateX(calc(-160px * 4)); + } + 75% { + transform: translateX(calc(-160px * 5)); + } + 83.33% { + transform: translateX(calc(-160px * 5)); + } + 91.66% { + transform: translateX(calc(-160px * 6)); + } + 100% { + transform: translateX(calc(-160px * 6)); + } + } +} + +.animate-scroll { + animation: scroll 30s linear infinite; +} + +/* Use case marquee - 自然な無限ループ */ +.usecase-marquee { + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + overflow: hidden; + background: #f9fafb; + padding: 24px 0; +} + +.usecase-track { + display: flex; + align-items: center; + gap: 32px; + white-space: nowrap; + animation: usecase-scroll 20s linear infinite; + width: 200%; /* シンプルに200%に戻す */ +} + +.usecase-item { + display: inline-flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + flex-shrink: 0; + min-width: 220px; /* 幅を調整して6個すべて表示されるように */ + justify-content: center; +} + +.usecase-logo { + font-size: 24px; + line-height: 1; +} + +.usecase-name { + font-size: 14px; + color: #374151; +} + +.usecase-logo-img { + object-fit: contain; +} + +/* ホバー時の一時停止 */ +.usecase-marquee:hover .usecase-track { + animation-play-state: paused; +} + +/* 自然な無限ループアニメーション */ +@keyframes usecase-scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } +} + +/* InfiniteLoopコンポーネント専用の滑らかなスクロールアニメーション */ +@keyframes infinite-scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } +} + +/* モバイル専用のInfiniteLoopアニメーション */ +@media (max-width: 1024px) { + @keyframes infinite-scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } + } +} + +/* ランディングページ用スタイル */ +main { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +section { + padding: 60px 20px; + text-align: center; +} + +section.hero { + text-align: center; +} + +h2 { + font-size: 2rem; + margin-bottom: 20px; +} + +ul, +ol { + text-align: left; + max-width: 600px; + margin: 0 auto; + padding-left: 20px; +} + +.hero { + display: flex; + align-items: center; + justify-content: center; + min-height: 60vh; +} + +.hero-content h1 { + font-size: 2.5rem; + margin-bottom: 20px; +} + +.hero-content p { + font-size: 1.25rem; + margin-bottom: 30px; + color: #444; +} + +.hero-buttons .btn-primary, +.hero-buttons .btn-secondary { + padding: 12px 24px; + font-size: 1rem; + border-radius: 6px; + cursor: pointer; + border: none; + min-width: 140px; + flex: 1; + max-width: 200px; + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.hero-buttons .btn-primary { + background-color: #f4b520; + color: #fff; +} + +.hero-buttons .btn-secondary { + background-color: #eee; + color: #222; +} + +/* ヒーローコンテンツ: 画像とボタンを重ねる */ +.hero-content { + position: relative; + display: flex; + justify-content: center; + align-items: center; +} + +/* ボタンを画像上に重ねて中央配置 */ +.hero-buttons { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + z-index: 1; + display: flex; + gap: 12px; +} + +/* ヒーロー画像のサイズ調整 */ +.hero-image { + max-width: 100%; + width: 100%; + height: auto; + display: block; +} + +.problem-grid, +.case-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + max-width: 900px; + margin: 0 auto; +} + +.problem-grid > div, +.case-card { + flex: 1 1 250px; + border: 1px solid #ddd; + border-radius: 8px; + padding: 20px; + background: #fafafa; +} + +ol li span { + font-weight: bold; + margin-right: 10px; +} + +.pricing table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +.pricing th, +.pricing td { + padding: 10px; + border: 1px solid #ccc; +} + +.faq dl { + max-width: 700px; + margin: 0 auto; + text-align: left; +} + +.faq dt { + font-weight: bold; + margin-top: 20px; +} + +.footer { + background: #222; + color: #fff; + padding: 20px; + text-align: center; +} + +.footer a { + color: #ccc; + margin: 0 5px; + text-decoration: none; +} + +/* レスポンシブ */ +@media (max-width: 768px) { + .problem-grid, + .case-grid { + flex-direction: column; + align-items: center; + } + + .hero-content h1 { + font-size: 1.8rem; + } + + .hero-content p { + font-size: 1rem; + } + + /* モバイルでのページとmainの中央配置修正 */ + .page { + padding: 20px 16px; + } + + main { + width: 100%; + max-width: 100%; + margin: 0; + padding: 0 8px; + } + + .hero-buttons .btn-primary, + .hero-buttons .btn-secondary { + min-width: 180px; + max-width: 300px; + padding: 10px 16px; + font-size: 0.9rem; + } + + /* モバイル: 画像を画面幅いっぱいに */ + .hero-image { + width: 100vw; + max-width: 100vw; + } + + /* モバイル用ユースケースマーキー */ + .usecase-track { + gap: 20px; + animation: usecase-scroll 15s linear infinite; /* 同じアニメーション名に統一 */ + width: 200%; /* シンプルに200%に統一 */ + } + + .usecase-item { + padding: 6px 12px; + gap: 8px; + min-width: 180px; /* 幅を増やして6個すべて表示されるように */ + } + + .usecase-name { + font-size: 12px; + } + + /* モバイル: ProblemSolution 追加改善 */ + section.problem { + padding: 40px 15px !important; + } + + section.problem h2 { + font-size: 1.3rem !important; + line-height: 1.5 !important; + margin-bottom: 30px !important; + } + + /* モバイル: HowItWorks 追加改善 */ + section.how { + padding: 40px 15px !important; + } + + section.how h2 { + font-size: 1.5rem !important; + margin-bottom: 35px !important; + } +} + +/* PC: 画像のオリジナルサイズを最大として中央配置 */ +@media (min-width: 769px) { + .hero-image { + width: auto; + max-width: none; + max-height: 80vh; + } +} + +/* セキュリティスタックのレスポンシブ対応 */ +.protocol-cards { + width: 100%; +} + +/* デスクトップではカードを横並びで表示 */ +@media (min-width: 992px) { + .protocol-cards > div { + display: flex !important; + flex-direction: row !important; + gap: 24px !important; + } + + .protocol-cards > div > div { + flex: 1; + min-width: 0; + } +} + +.casestudies .case-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + max-width: 1000px; + margin: 0 auto; +} + +.casestudies .case-card { + flex: 1 1 280px; + border: 1px solid #ddd; + border-radius: 8px; + padding: 20px; + background: #fff; + text-align: left; +} + +.casestudies .case-logo { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 15px; + height: 100px; +} + +.casestudies .logo-image { + object-fit: contain; +} + +.casestudies .logo-placeholder { + display: flex; + justify-content: center; + align-items: center; + width: 80px; + height: 80px; + border: 2px dashed #ddd; + border-radius: 8px; + background: #f9f9f9; +} + +.casestudies h3 { + margin: 0; + font-size: 1.2rem; + color: #22a186; +} + +.casestudies .case-name { + text-align: center; + margin-bottom: 15px; +} + +.casestudies h4 { + margin: 10px 0; + font-size: 1rem; + font-weight: bold; +} + +/* TrackRecord セクションのスタイル */ +.trackrecord { + padding: 60px 36px; + text-align: center; +} + +@media (max-width: 768px) { + .casestudies .case-grid { + flex-direction: column; + align-items: center; + } + + .casestudies .case-card { + width: 90%; + max-width: 350px; + flex: none; + } +} + +/* 新しいページレイアウトスタイル */ +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + /* grid-template-rows: 20px 1fr 20px; */ + align-items: center; + justify-items: center; + min-height: 100svh; + /* padding: 80px; */ + /* gap: 64px; */ + font-family: var(--font-geist-sans); + width: 100%; + max-width: 100vw; + overflow-x: hidden; +} + +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } +} + +.main { + display: flex; + flex-direction: column; + /* gap: 32px; */ + grid-row-start: 2; + width: 100%; + max-width: 1200px; + margin: 0 auto; + box-sizing: border-box; +} + +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; + margin: 0; + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; +} + +.main li:not(:last-of-type) { + margin-bottom: 8px; +} + +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; +} + +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: 1px solid transparent; + transition: background 0.2s, color 0.2s, border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; +} + +a.primary { + background: var(--foreground); + color: var(--background); + gap: 8px; +} + +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 158px; +} + +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; +} + +.footer a { + display: flex; + align-items: center; + gap: 8px; +} + +.footer img { + flex-shrink: 0; +} + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; + } + + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; + } + + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; + } +} + +@media (max-width: 600px) { + .page { + padding: 16px; + padding-bottom: 80px; + } + + .main { + align-items: center; + width: 100%; + max-width: 100%; + margin: 0; + padding: 0 8px; + } + + .main ol { + text-align: center; + } + + .ctas { + flex-direction: column; + } + + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; + } + + a.secondary { + min-width: auto; + } + + .footer { + flex-wrap: wrap; + align-items: center; + justify-content: center; + } +} + +@media (prefers-color-scheme: dark) { + .logo { + filter: invert(); + } +} + +/* モバイル画面での確実な中央配置 */ +@media screen and (max-width: 1024px) { + * { + box-sizing: border-box !important; + } + + html, + body { + width: 100% !important; + max-width: 100vw !important; + overflow-x: hidden !important; + margin: 0 !important; + padding: 0 !important; + } + + .page { + padding: 0 !important; + width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + box-sizing: border-box !important; + justify-items: center !important; + align-items: center !important; + overflow-x: hidden !important; + } + + .main { + width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + padding: 0 !important; + box-sizing: border-box !important; + overflow-x: hidden !important; + } + + main { + width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + padding: 0 !important; + box-sizing: border-box !important; + overflow-x: hidden !important; + } + + /* 各セクションの強制的な中央配置 */ + section { + width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + padding: 40px 16px !important; + box-sizing: border-box !important; + overflow-x: hidden !important; + position: relative !important; + left: 0 !important; + right: 0 !important; + } + + /* ヒーローセクションの特別対応 */ + .hero { + width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + /* padding: 20px 16px !important; */ + box-sizing: border-box !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + overflow-x: hidden !important; + position: relative !important; + left: 0 !important; + right: 0 !important; + } + + .hero-content { + width: 100% !important; + max-width: calc(100vw - 32px) !important; + margin: 0 !important; + padding: 0 !important; + box-sizing: border-box !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + } + + .hero-image { + width: 100% !important; + max-width: 100% !important; + height: auto !important; + object-fit: contain !important; + } + + /* すべてのコンテナの強制的な幅調整 */ + div { + max-width: 100vw !important; + box-sizing: border-box !important; + } + + /* インラインスタイルを持つコンテナの上書き */ + div[style*="maxWidth"] { + max-width: calc(100vw - 32px) !important; + } + + div[style*="margin: 0 auto"] { + margin: 0 16px !important; + } + + /* Grid系レイアウトの調整 */ + div[style*="display: grid"], + div[style*="gridTemplateColumns"] { + grid-template-columns: 1fr !important; + width: 100% !important; + max-width: calc(100vw - 32px) !important; + } + + /* Flex系レイアウトの調整 */ + div[style*="display: flex"]:not(.logo-track-container):not( + .infinite-loop-wrapper + ) { + flex-wrap: wrap !important; + width: 100% !important; + max-width: calc(100vw - 32px) !important; + } + + /* InfiniteLoopコンポーネントの特別対応 */ + .infinite-loop-container { + width: 100% !important; + max-width: 100vw !important; + margin: 0 !important; + overflow-x: hidden !important; + } + + .infinite-loop-wrapper { + width: 100% !important; + max-width: 100vw !important; + margin: 0 auto !important; + overflow-x: hidden !important; + } + + .logo-track-container { + display: flex !important; + flex-wrap: nowrap !important; + width: calc(200% + 40px) !important; + max-width: none !important; + align-items: center !important; + animation-name: infinite-scroll !important; + } + + /* アニメーション要素の保護 */ + div[style*="animationName"] { + flex-wrap: nowrap !important; + max-width: none !important; + } +} diff --git a/src/types/index.tsx b/src/types/index.tsx new file mode 100644 index 0000000..9a20234 --- /dev/null +++ b/src/types/index.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; + +export interface PageProps { + pageTitle: string; + params?: { + id: string[]; + }; + searchParams?: { [key: string]: string | string[] | undefined }; + children?: ReactNode; +} + +export interface ComponentProps { + children?: ReactNode; +} + +interface Contributor { + name: string; + linkToOnlinePresence: string; + role: string; + org: string; + orgUrl: string; + imagePath: string; +} + +export interface Mentor extends Contributor {} + +export interface Judge extends Contributor {} diff --git a/yarn.lock b/yarn.lock index b004e5f..c9d27a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1861,6 +1861,15 @@ __metadata: languageName: node linkType: hard +"react-icons@npm:^5.5.0": + version: 5.5.0 + resolution: "react-icons@npm:5.5.0" + peerDependencies: + react: "*" + checksum: cbd74f4b7982e6e18d59798a6b578268c8eb0909d78d87bcf9b25f99b3e544fd189a76551cb5e770d17f154a60b668551aee108aaf8471309b23f7af3b2c5b07 + languageName: node + linkType: hard + "react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -2312,6 +2321,7 @@ __metadata: next: ^15.0.0 react: ^18.0.0 react-dom: ^18.0.0 + react-icons: ^5.5.0 serve: ^14.0.0 typescript: ^5.0.0 languageName: unknown