Add Wakatime component and integrate with About section
This commit is contained in:
parent
9eda12d5ed
commit
ae90712b97
5 changed files with 125 additions and 29 deletions
|
|
@ -6,6 +6,7 @@ import Contact from "@/components/Home/Contact";
|
||||||
import Project from "@/components/Home/Project";
|
import Project from "@/components/Home/Project";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import Wakatime from "./components/Home/Wakatime";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -66,6 +67,9 @@ export function App() {
|
||||||
<div id="about" className="section">
|
<div id="about" className="section">
|
||||||
<About />
|
<About />
|
||||||
</div>
|
</div>
|
||||||
|
<div id="wakatime" className="section">
|
||||||
|
<Wakatime />
|
||||||
|
</div>
|
||||||
<div id="project" className="section">
|
<div id="project" className="section">
|
||||||
<Project />
|
<Project />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const [time, setTime] = useState<string>("");
|
const [wakatime, setWakatime] = useState<any>();
|
||||||
|
const [time, setTime] = useState<number>(0);
|
||||||
const [post, setPost] = useState<any>({});
|
const [post, setPost] = useState<any>({});
|
||||||
const [age, setAge] = useState<number>(0);
|
const [age, setAge] = useState<number>(0);
|
||||||
|
const [totalSeconds, setTotalSeconds] = useState<number>(0);
|
||||||
|
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||||
|
const AboutRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Calculate age based on reference date (2010-11-08)
|
// 나이 계산
|
||||||
const referenceDate = new Date(2010, 11, 8); // November is 10 because months are 0-indexed
|
const referenceDate = new Date(2010, 10, 8); // 2010년 11월 8일 (0-indexed)
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
let calculatedAge = currentDate.getFullYear() - referenceDate.getFullYear();
|
let calculatedAge = currentDate.getFullYear() - referenceDate.getFullYear();
|
||||||
if (currentDate < new Date(currentDate.getFullYear(), referenceDate.getMonth(), referenceDate.getDate())) {
|
if (currentDate < new Date(currentDate.getFullYear(), referenceDate.getMonth(), referenceDate.getDate())) {
|
||||||
|
|
@ -17,32 +21,57 @@ export default function About() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("https://api.imnya.ng/rss", {
|
// 블로그 데이터 가져오기
|
||||||
method: "GET",
|
fetch("https://api.imnya.ng/rss")
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data) {
|
if (data) setPost(data[0] || {});
|
||||||
setPost(data[0] || {});
|
|
||||||
} else {
|
|
||||||
console.error("Error: data is undefined");
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error fetching posts:", error));
|
.catch(error => console.error("Error fetching posts:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
// Intersection Observer로 isVisible 상태 변경
|
||||||
setTime(new Date().toLocaleTimeString('en-US', { timeZone: 'Asia/Seoul', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }));
|
const observer = new IntersectionObserver(
|
||||||
}, 1);
|
([entry]) => {
|
||||||
return () => clearInterval(interval);
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (AboutRef.current) observer.observe(AboutRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (AboutRef.current) observer.unobserve(AboutRef.current);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Wakatime 데이터 가져오기 (한 번만 실행)
|
||||||
|
fetch("https://api.imnya.ng/wakatime")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
|
const roundedSeconds = Math.round(data.data.total_seconds); // 반올림
|
||||||
|
setTotalSeconds(roundedSeconds);
|
||||||
|
setWakatime(data.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Error fetching Wakatime data:", error));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// isVisible이 true일 때 time 증가
|
||||||
|
if (isVisible && time < totalSeconds) {
|
||||||
|
const timer = setTimeout(() => setTime(prevTime => prevTime + 1), time === 0 ? 300 : 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isVisible, time, totalSeconds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen flex flex-col items-center justify-center">
|
<div className="w-full h-screen flex flex-col items-center justify-center" ref={AboutRef}>
|
||||||
<div className="w-full md:w-[50%] p-4">
|
<div className="w-full md:w-[50%] p-4">
|
||||||
<h1 className="text-2xl font-bold">🤔 About</h1>
|
<h1 className="text-2xl font-bold">🤔 About</h1>
|
||||||
<div className="flex items-start justify-center flex-col p-2 mt-2 w-full">
|
<div className="flex items-start justify-center flex-col p-2 mt-2 w-full">
|
||||||
|
|
@ -51,15 +80,21 @@ export default function About() {
|
||||||
<h1>삶을 더 <strong className="font-black">간단명료</strong>하게 만들고 있는</h1>
|
<h1>삶을 더 <strong className="font-black">간단명료</strong>하게 만들고 있는</h1>
|
||||||
<h1>학생 개발자 <strong className="font-black">남현석</strong>입니다.</h1>
|
<h1>학생 개발자 <strong className="font-black">남현석</strong>입니다.</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<p>{age}살의 어린 나이지만</p>
|
<p>{age}살의 어린 나이지만</p>
|
||||||
<p>저는 항상 제가 할 수 있는 최적의 코드를 목표로 하며</p>
|
<p>저는 항상 제가 할 수 있는 최적의 코드를 목표로 하고</p>
|
||||||
<p>사용자의 경험을 중심적으로 고려하며</p>
|
<p>사용자의 경험을 중심적으로 고려하며</p>
|
||||||
<p>새로운 기술에 대한 관심이 높습니다.</p>
|
<p>새로운 기술에 대한 관심이 높습니다.</p>
|
||||||
|
|
||||||
<h1 className="mt-4 text-foreground">In South Korea : <span className="tnum text-muted-foreground">{time}</span></h1>
|
<br/>
|
||||||
<h1 className="text-foreground">최근 블로그 보기 : <a href={post.link} className="text-muted-foreground">{post.title}</a></h1>
|
|
||||||
|
<a className="mt-4 text-foreground" href="https://wakatime.com/@imnyang" target="_blank" rel="noopener noreferrer">
|
||||||
|
Wakatime Mar 18th ~ : <span className="tnum text-muted-foreground">{time}</span>s
|
||||||
|
</a>
|
||||||
|
<h1 className="text-foreground">
|
||||||
|
최근 블로그 보기 : <a href={post.link} className="text-muted-foreground">{post.title}</a>
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
40
src/components/Home/Wakatime.tsx
Normal file
40
src/components/Home/Wakatime.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function Wakatime() {
|
||||||
|
const [wakatime, setWakatime] = useState<any>();
|
||||||
|
useEffect(() => {
|
||||||
|
// Wakatime 데이터 가져오기 (한 번만 실행)
|
||||||
|
fetch("https://api.imnya.ng/wakatime")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
|
setWakatime(data.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Error fetching Wakatime data:", error));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen flex flex-col items-center justify-center">
|
||||||
|
<div className="w-full md:w-[50%] p-4">
|
||||||
|
<a className="text-2xl font-bold" href="https://wakatime.com/@imnyang" target="_blank" rel="noopener noreferrer">🍝 Wakatime</a>
|
||||||
|
<p>Dashboards for developers</p>
|
||||||
|
<br />
|
||||||
|
{wakatime && wakatime.languages && (
|
||||||
|
<div>
|
||||||
|
<p>총 시간: {(wakatime.human_readable_total)}</p>
|
||||||
|
<p>하루 평균: {wakatime.human_readable_daily_average}</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>가장 많이 사용한 언어:</p>
|
||||||
|
<ul>
|
||||||
|
{wakatime.languages.slice(0, 3).map((language: any, index: number) => (
|
||||||
|
<li key={index}>{index+1}. {language.name}: {language.percent}%</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<!--
|
||||||
|
이번 패치하면서 들은 곡
|
||||||
|
Laur - Gears of Fate [AWC 2025 Finals Tiebreaker]
|
||||||
|
https://www.youtube.com/watch?v=-bnrmxa2dW0
|
||||||
|
-->
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
|
||||||
<title>남현석 | :two_hearts:</title>
|
<title>남현석 | :two_hearts:</title>
|
||||||
<meta name="description" content="항상 탐구하고 연구하는 평범한 학생 개발자입니다." />
|
<meta name="description" content="항상 탐구하고 연구하는 평범한 학생 개발자입니다." />
|
||||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4588517451789913" crossorigin="anonymous"></script>
|
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4588517451789913" crossorigin="anonymous"></script>
|
||||||
<script type="module" src="./frontend.tsx" async></script>
|
<script type="module" src="./frontend.tsx" async></script>'
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fixed-width-number {
|
.fixed-width-number {
|
||||||
font-feature-settings: "tnum";
|
font-feature-settings: "tnum";
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue