Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: 소개 페이지 구현 #276

Merged
merged 10 commits into from
Dec 5, 2024
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from "react";
import { Routes, Route } from "react-router-dom";

import AboutService from "@/pages/AboutService";
import Admin from "@/pages/Admin";
import Home from "@/pages/Home";

Expand Down Expand Up @@ -55,6 +56,7 @@ export default function App() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<Admin />} />
<Route path="/about" element={<AboutService />} />
</Routes>
<ReactQueryDevtools />
</QueryClientProvider>
Expand Down
31 changes: 31 additions & 0 deletions client/src/components/about/CTASection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Link } from "react-router-dom";

import { Section } from "@/components/about/Section.tsx";
import { Button } from "@/components/ui/button";

export const CTASection = () => {
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};

return (
<Section id="cta-section" className="min-h-screen flex items-center px-8">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-3xl font-bold mb-6">지금 바로 시작하세요</h2>
<p className="text-gray-600 mb-8">개발자들의 인사이트 가득한 블로그 세상으로 초대합니다</p>
<div className="flex items-center justify-center space-x-4">
<Link to="/">
<Button size="lg">블로그 둘러보기</Button>
</Link>

<Button variant="outline" size="lg" onClick={scrollToTop}>
다시 알아보기
</Button>
</div>
</div>
</Section>
);
};
48 changes: 48 additions & 0 deletions client/src/components/about/ExtraFeatureSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Section } from "@/components/about/Section";

import { EXTRA_FEATURES } from "@/constants/about";

export const ExtraFeatureSection = () => {
return (
<Section className="py-20 px-8 bg-gray-50">
<div className="max-w-6xl mx-auto">
<div className="mb-16">
<h1 className="text-l font-bold mb-2 text-primary">{EXTRA_FEATURES.mainTitle}</h1>
<p className="text-2xl font-semibold whitespace-pre-line leading-normal">{EXTRA_FEATURES.groupTitle}</p>
</div>

<div className="space-y-20">
{EXTRA_FEATURES.sections.map((section, idx) => (
<div key={idx} className="space-y-8">
<h2 className="text-2xl font-bold text-gray-800">{section.title}</h2>

<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{section.features.map((feature, featureIdx) => (
<div key={featureIdx} className="bg-white rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-primary/10 rounded-lg">
<feature.icon className="w-6 h-6 text-primary" />
</div>
<h3 className="text-xl font-semibold">{feature.shortTitle}</h3>
</div>

<p className="text-gray-600 mb-6">{feature.description}</p>

<ul className="space-y-3">
{feature.items.map((item, itemIdx) => (
<li key={itemIdx} className="flex items-center gap-2 text-gray-600">
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
{item}
</li>
))}
</ul>
</div>
))}
</div>
</div>
))}
</div>
</div>
</Section>
);
};
42 changes: 42 additions & 0 deletions client/src/components/about/FeatureCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useInView } from "@/hooks/common/useInView";

import { cn } from "@/lib/utils";
import { FeatureItem } from "@/types/about";

interface FeatureCardProps {
feature: FeatureItem;
}

export const FeatureCard = ({ feature }: FeatureCardProps) => {
const { ref, isInView } = useInView<HTMLDivElement>({ once: true });
const Icon = feature.icon;

return (
<div
ref={ref}
className={cn(
"transition-all duration-1000 ease-out",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
)}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{feature.imageSrc ? (
<img src={feature.imageSrc} alt={feature.imageAlt} className="w-full aspect-video object-cover" />
) : (
<div className="w-full aspect-video flex items-center justify-center">
<span>이미지 준비 중</span>
</div>
)}

<div className="flex flex-col justify-center">
<div className="flex items-center gap-2">
<Icon className="w-4 h-8 text-primary" />
<h4 className="text-l font-semibold text-primary">{feature.shortTitle}</h4>
</div>
<h3 className="text-2xl font-bold">{feature.longTitle}</h3>
<p className="text-lg pt-6 text-gray-400 font-semibold whitespace-pre-line">{feature.description}</p>
</div>
</div>
</div>
);
};
31 changes: 31 additions & 0 deletions client/src/components/about/FeatureSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Section } from "@/components/about/Section.tsx";

import { FEATURES } from "@/constants/about";

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

export const FeatureSection = () => {
return (
<div>
{FEATURES.map((feature, idx) => (
<Section key={idx} className={cn("py-20 px-8", idx % 2 === 0 ? "bg-white" : "bg-gray-50")}>
<div className="max-w-6xl mx-auto">
<div className="space-y-16">
<div className="mb-12">
<p className="text-l font-bold mb-2 text-primary">{feature.mainTitle}</p>
<h2 className="text-2xl font-semibold whitespace-pre-line leading-normal">{feature.groupTitle}</h2>
</div>

<div className="space-y-20">
{feature.features.map((featureItem, itemIdx) => (
<FeatureCard key={itemIdx} feature={featureItem} />
))}
</div>
</div>
</div>
</Section>
))}
</div>
);
};
57 changes: 57 additions & 0 deletions client/src/components/about/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { footerLinks, teamMembers } from "@/constants/footer";

import type { FooterLink } from "@/types/footer";

export const Footer = () => {
const renderFooterLink = (link: FooterLink) => (
<div className="flex flex-col space-y-2">
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col space-y-2 hover:text-primary transition-colors"
>
<div className="flex items-center gap-2 text-gray-500">
<link.icon className="w-4 h-4" />
<span className="text-sm">{link.label}</span>
</div>
<span className="text-sm font-medium">{link.value}</span>
</a>
{link.subLinks?.map((subLink, subIdx) => (
<a
key={subIdx}
href={subLink.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-primary transition-colors ml-6"
>
{subLink.label}
</a>
))}
</div>
);

return (
<footer className="bg-gray-50 border-t border-gray-200">
<div className="max-w-6xl mx-auto px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{footerLinks.map((link, idx) => (
<div key={idx}>{renderFooterLink(link)}</div>
))}
</div>

<div className="mt-8 pt-8">
<div className="text-center text-[10px] text-gray-500">
<span>Made by </span>
{teamMembers.map((name, idx) => (
<span key={idx}>
{name}
{idx !== teamMembers.length - 1 && ", "}
</span>
))}
</div>
</div>
</div>
</footer>
);
};
48 changes: 48 additions & 0 deletions client/src/components/about/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ChevronsDown } from "lucide-react";

import { Section } from "@/components/about/Section.tsx";
import { Button } from "@/components/ui/button";

import logo from "@/assets/logo-denamu-main.svg";

export const HeroSection = () => {
const scrollToBottom = () => {
const ctaSection = document.querySelector("#cta-section");
if (ctaSection) {
ctaSection.scrollIntoView({ behavior: "smooth" });
}
};

return (
<Section className="min-h-screen relative flex flex-col">
<div className="flex-shrink-0 min-h-[400px] md:min-h-[45vh] flex items-center justify-center p-8">
<div className="text-center max-w-4xl">
<div className="flex items-center justify-center space-x-2 mb-6">
<img src={logo} className="w-32 md:w-52" />
</div>
<h1 className="text-3xl md:text-5xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-[#27ae60] via-[#2596be] to-[#228be6]">
개발자를 위한 최고의 블로그 허브
</h1>
<p className="text-lg md:text-xl mb-8">기술 블로그를 한눈에 모아보고, 개발자들과 함께 성장하세요.</p>
<div className="flex items-center justify-center space-x-4">
<Button size="lg" onClick={scrollToBottom}>
바로 시작하기
</Button>
</div>
</div>
</div>

<div className="flex-grow flex items-start justify-center p-4 md:p-8">
<img
src="https://github.com/user-attachments/assets/e91e80c0-1c1b-40d0-ae6c-0f0151e03796"
alt="Service Preview"
className="max-w-[90%] md:max-w-[80%] h-auto object-contain"
/>
</div>

<div className="absolute bottom-4 md:bottom-8 left-1/2 -translate-x-1/2">
<ChevronsDown className="w-8 h-8 md:w-12 md:h-12 text-gray-400 animate-slow-bounce" />
</div>
</Section>
);
};
29 changes: 29 additions & 0 deletions client/src/components/about/Section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { HTMLAttributes, forwardRef } from "react";

import { useInView } from "@/hooks/common/useInView.ts";

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

interface SectionProps extends HTMLAttributes<HTMLElement> {
children: React.ReactNode;
}

export const Section = forwardRef<HTMLElement, SectionProps>(({ children, className, ...props }) => {
const { ref, isInView } = useInView<HTMLElement>({ once: true });

return (
<section
ref={ref}
className={cn(
"transition-all duration-1000",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10",
className
)}
{...props}
>
{children}
</section>
);
});

Section.displayName = "Section";
Loading
Loading