Skip to content

Commit

Permalink
Aboutページを追加し、言語の壁を越える機能を紹介するセクションを実装しました。また、Globeコンポーネントを追加し、インタラクティブ…
Browse files Browse the repository at this point in the history
…な地球儀を表示できるようにしました。依存関係にcobeとmotionを追加しました。
  • Loading branch information
ttizze committed Mar 1, 2025
1 parent 46c817b commit 2928e16
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 1 deletion.
Binary file modified next/bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@
"cld3-asm": "4.0.0",
"clsx": "2.1.1",
"cmdk": "1.0.4",
"cobe": "0.6.3",
"front-matter": "4.0.2",
"hast": "1.0.0",
"hast-util-to-html": "9.0.5",
"html-react-parser": "5.2.2",
"isomorphic-dompurify": "2.22.0",
"linkify-react": "4.2.0",
"lucide-react": "0.476.0",
"motion": "^12.4.7",
"nanoid": "5.1.0",
"next": "15.2.0",
"next-auth": "5.0.0-beta.25",
Expand Down
199 changes: 199 additions & 0 deletions next/src/app/[locale]/(common-layout)/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { Globe } from "@/components/magicui/globe";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Book, Code, Heart, MessageSquare, Users } from "lucide-react";
import type { Metadata } from "next";
import dynamic from "next/dynamic";

const HeroSection = dynamic(
() => import("@/app/[locale]/components/hero-section/index"),
{
loading: () => <Skeleton className="h-[845px] w-full" />,
},
);

export const metadata: Metadata = {
title: "Evame - About",
description:
"Evame is an open-source platform for collaborative article translation and sharing.",
};

const features = [
{
icon: <Book className="h-10 w-10 text-primary" />,
title: "Automatic Translation",
description:
"Articles and comments are automatically translated to multiple languages, breaking down language barriers.",
},
{
icon: <Book className="h-10 w-10 text-primary" />,
title: "Multilingual Content",
description:
"Share your thoughts in any language and reach a global audience instantly.",
},
{
icon: <MessageSquare className="h-10 w-10 text-primary" />,
title: "Real-time Comments",
description:
"Engage in discussions with people from around the world in your preferred language.",
},
{
icon: <Users className="h-10 w-10 text-primary" />,
title: "Global Community",
description:
"Connect with diverse perspectives and ideas from across cultural boundaries.",
},
{
icon: <Code className="h-10 w-10 text-primary" />,
title: "Open Source",
description:
"Built with transparency and collaboration in mind, open for contributions from everyone.",
},
{
icon: <Heart className="h-10 w-10 text-primary" />,
title: "Community Driven",
description:
"Shaped by the needs and feedback of our diverse global community of users.",
},
];

export default async function AboutPage({
params,
searchParams,
}: {
params: Promise<{ locale: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { locale } = await params;

// Translation function placeholder - would be implemented with actual translation service
const t = (key: string) => {
const translations: { [key: string]: { [key: string]: string } } = {
"about.title": {
en: "About Evame",
ja: "Evameについて",
fr: "À propos d'Evame",
es: "Acerca de Evame",
zh: "关于Evame",
},
"about.subtitle": {
en: "Breaking language barriers through technology",
ja: "テクノロジーで言語の壁を壊す",
fr: "Briser les barrières linguistiques grâce à la technologie",
es: "Rompiendo barreras lingüísticas a través de la tecnología",
zh: "通过技术打破语言障碍",
},
"about.mission": {
en: "Our Mission",
ja: "私たちのミッション",
fr: "Notre Mission",
es: "Nuestra Misión",
zh: "我们的使命",
},
"about.mission.text": {
en: "Evame aims to create a world where language is no longer a barrier to communication and knowledge sharing. Through automatic translation technology, we enable people from different linguistic backgrounds to connect, share ideas, and learn from each other.",
ja: "Evameは、言語がコミュニケーションや知識共有の障壁ではない世界を作ることを目指しています。自動翻訳技術を通じて、異なる言語背景を持つ人々が繋がり、アイデアを共有し、お互いから学ぶことを可能にします。",
fr: "Evame vise à créer un monde où la langue n'est plus un obstacle à la communication et au partage des connaissances. Grâce à la technologie de traduction automatique, nous permettons aux personnes de différentes origines linguistiques de se connecter, de partager des idées et d'apprendre les unes des autres.",
es: "Evame aspira a crear un mundo donde el idioma ya no sea una barrera para la comunicación y el intercambio de conocimientos. A través de la tecnología de traducción automática, permitimos que personas de diferentes orígenes lingüísticos se conecten, compartan ideas y aprendan unas de otras.",
zh: "Evame旨在创建一个语言不再成为沟通和知识共享障碍的世界。通过自动翻译技术,我们使不同语言背景的人们能够连接、分享想法并相互学习。",
},
"about.features": {
en: "Features",
ja: "特徴",
fr: "Fonctionnalités",
es: "Características",
zh: "功能",
},
"about.join": {
en: "Join Our Community",
ja: "コミュニティに参加する",
fr: "Rejoignez Notre Communauté",
es: "Únete a Nuestra Comunidad",
zh: "加入我们的社区",
},
"about.join.text": {
en: "Be part of a growing global network of individuals passionate about cross-cultural communication and knowledge sharing.",
ja: "異文化間コミュニケーションと知識共有に情熱を持つ個人の成長するグローバルネットワークの一員になりましょう。",
fr: "Faites partie d'un réseau mondial croissant d'individus passionnés par la communication interculturelle et le partage des connaissances.",
es: "Forma parte de una creciente red global de personas apasionadas por la comunicación intercultural y el intercambio de conocimientos.",
zh: "成为对跨文化交流和知识共享充满热情的个人不断发展的全球网络的一部分。",
},
"about.join.button": {
en: "Sign Up Now",
ja: "今すぐサインアップ",
fr: "Inscrivez-vous Maintenant",
es: "Regístrate Ahora",
zh: "立即注册",
},
};

return translations[key][locale] || translations[key].en;
};

return (
<div className="flex flex-col justify-between">
<HeroSection locale={locale} />
<div className="relative flex size-full max-w-lg items-center justify-center overflow-hidden rounded-lg border bg-background px-40 pb-40 pt-8 md:pb-60">
<span className="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10">
World
</span>
<Globe className="top-28" />
<div className="pointer-events-none absolute inset-0 h-full bg-[radial-gradient(circle_at_50%_200%,rgba(0,0,0,0.2),rgba(255,255,255,0))]" />
</div>
<div className="container mx-auto px-4 py-16">
<div className="text-center mb-16">
<h1 className="text-4xl font-bold mb-4">{t("about.title")}</h1>
<p className="text-xl text-muted-foreground">{t("about.subtitle")}</p>
</div>

<div className="mb-20">
<h2 className="text-3xl font-bold mb-6 text-center">
{t("about.mission")}
</h2>
<p className="text-lg max-w-3xl mx-auto text-center">
{t("about.mission.text")}
</p>
</div>

<div className="mb-20">
<h2 className="text-3xl font-bold mb-10 text-center">
{t("about.features")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<Card
key={feature.title}
className="hover:shadow-lg transition-shadow"
>
<CardHeader className="flex flex-row items-center gap-4">
{feature.icon}
<CardTitle>{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-base">
{feature.description}
</CardDescription>
</CardContent>
</Card>
))}
</div>
</div>

<div className="bg-primary/10 rounded-lg p-8 text-center max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-4">{t("about.join")}</h2>
<p className="mb-6">{t("about.join.text")}</p>
<Button size="lg" className="font-medium">
{t("about.join.button")}
</Button>
</div>
</div>
</div>
);
}
1 change: 0 additions & 1 deletion next/src/app/[locale]/components/start-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"use client";
import { Button } from "@/components/ui/button";
import { Link } from "@/i18n/routing";
interface StartButtonProps {
Expand Down
134 changes: 134 additions & 0 deletions next/src/components/magicui/globe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"use client";

import createGlobe, { type COBEOptions } from "cobe";
import { useMotionValue, useSpring } from "motion/react";
import { useEffect, useRef } from "react";

import { cn } from "@/lib/utils";

const MOVEMENT_DAMPING = 1400;

const GLOBE_CONFIG: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.3,
dark: 0,
diffuse: 0.4,
mapSamples: 16000,
mapBrightness: 1.2,
baseColor: [1, 1, 1],
markerColor: [251 / 255, 100 / 255, 21 / 255],
glowColor: [1, 1, 1],
markers: [
{ location: [14.5995, 120.9842], size: 0.03 },
{ location: [19.076, 72.8777], size: 0.1 },
{ location: [23.8103, 90.4125], size: 0.05 },
{ location: [30.0444, 31.2357], size: 0.07 },
{ location: [39.9042, 116.4074], size: 0.08 },
{ location: [-23.5505, -46.6333], size: 0.1 },
{ location: [19.4326, -99.1332], size: 0.1 },
{ location: [40.7128, -74.006], size: 0.1 },
{ location: [34.6937, 135.5022], size: 0.05 },
{ location: [41.0082, 28.9784], size: 0.06 },
],
};

export function Globe({
className,
config = GLOBE_CONFIG,
}: {
className?: string;
config?: COBEOptions;
}) {
let phi = 0;
let width = 0;
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerInteractionMovement = useRef(0);

const r = useMotionValue(0);
const rs = useSpring(r, {
mass: 1,
damping: 30,
stiffness: 100,
});

const updatePointerInteraction = (value: number | null) => {
pointerInteracting.current = value;
if (canvasRef.current) {
canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab";
}
};

const updateMovement = (clientX: number) => {
if (pointerInteracting.current !== null) {
const delta = clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
r.set(r.get() + delta / MOVEMENT_DAMPING);
}
};

useEffect(() => {
const onResize = () => {
if (canvasRef.current) {
width = canvasRef.current.offsetWidth;
}
};

window.addEventListener("resize", onResize);
onResize();

if (!canvasRef.current) return;

const globe = createGlobe(canvasRef.current, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!pointerInteracting.current) phi += 0.005;
state.phi = phi + rs.get();
state.width = width * 2;
state.height = width * 2;
},
});

// Set opacity after component mounts
if (canvasRef.current) {
canvasRef.current.style.opacity = "1";
}

return () => {
globe.destroy();
window.removeEventListener("resize", onResize);
};
}, [rs, config, phi, width]);

return (
<div
className={cn(
"absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]",
className,
)}
>
<canvas
className={cn(
"size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]",
)}
ref={canvasRef}
onPointerDown={(e) => {
pointerInteracting.current = e.clientX;
updatePointerInteraction(e.clientX);
}}
onPointerUp={() => updatePointerInteraction(null)}
onPointerOut={() => updatePointerInteraction(null)}
onMouseMove={(e) => updateMovement(e.clientX)}
onTouchMove={(e) =>
e.touches[0] && updateMovement(e.touches[0].clientX)
}
/>
</div>
);
}

0 comments on commit 2928e16

Please sign in to comment.