refactor: reorganize components and move files to appropriate directories
This commit is contained in:
parent
b270dd1d21
commit
43a3d944ea
12 changed files with 7 additions and 7 deletions
192
app/about/components/education.jsx
Normal file
192
app/about/components/education.jsx
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import Image from "next/image";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMedal } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import Me4 from "@/public/img/me4.jpeg";
|
||||
|
||||
export default function Education() {
|
||||
return (
|
||||
<section className="grid gap-8 md:gap-12">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tighter">
|
||||
Education
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-[800px] mx-auto">
|
||||
Get to know more about my educational background.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-8 md:gap-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="px-5">
|
||||
<div className="font-medium text-lg">
|
||||
2022 - Present
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-semibold text-xl">
|
||||
Universitas Negeri Malang
|
||||
</h2>
|
||||
<h3 className="text-md font-normal mb-3">
|
||||
S1 Teknik Informatika | Computer Science
|
||||
</h3>
|
||||
<div className="gap-4 mb-4 flex items-stretch md:h-[300px] xl:h-[400px]">
|
||||
<div className="flex-[1] transition-all duration-300 ease-in-out hover:flex-[3] group">
|
||||
<Image
|
||||
src={Me4}
|
||||
width={400}
|
||||
height={225}
|
||||
alt="University"
|
||||
className="rounded-lg w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-300 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[1] transition-all duration-300 ease-in-out hover:flex-[3] group">
|
||||
<Image
|
||||
src={Me4}
|
||||
width={400}
|
||||
height={225}
|
||||
alt="University"
|
||||
className="rounded-lg w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-300 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[1] transition-all duration-300 ease-in-out hover:flex-[3] group">
|
||||
<Image
|
||||
src={Me4}
|
||||
width={400}
|
||||
height={225}
|
||||
alt="University"
|
||||
className="rounded-lg w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-300 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-justify">
|
||||
Aspiring to be a software engineer, I am
|
||||
currently pursuing a bachelor’s degree
|
||||
in computer science at Universitas Negeri
|
||||
Malang. Fascinated by the world of
|
||||
technology, I am eager to learn and explore
|
||||
new things in the field of computer science.
|
||||
</p>
|
||||
</div>
|
||||
{/* add gpa in a style */}
|
||||
<div className="flex flex-wrap gap-2 mt-4 text-sm">
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
GPA: 3.9 out of 4
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start px-5 md:px-0">
|
||||
<h2 className="font-semibold text-xl mt-7">
|
||||
Achievements
|
||||
</h2>
|
||||
<p className="text-md font-normal mb-3 md:mb-12">
|
||||
Some of my achievements during my study.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="aspect-square w-10 rounded-full bg-black flex items-center justify-center text-primary-foreground">
|
||||
<FontAwesomeIcon
|
||||
icon={faMedal}
|
||||
className="text-white h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
Special Award | Gold Medal | Incubation
|
||||
Opportunity
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
Indonesia Inventor Day 2024 (IID) | Aug 2024
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="aspect-square w-10 rounded-full bg-black flex items-center justify-center text-primary-foreground">
|
||||
<FontAwesomeIcon
|
||||
icon={faMedal}
|
||||
className="text-white h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
3rd place (Bronze Medal)
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
Student Digital Innovation Competition
|
||||
(LIDM) | Jun 2024
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="aspect-square w-10 rounded-full bg-black flex items-center justify-center text-primary-foreground">
|
||||
<FontAwesomeIcon
|
||||
icon={faMedal}
|
||||
className="text-white h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
3rd place (Bronze Medal)
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
Fesmaro UI/UX Design | Jun 2024
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
<div className="aspect-square w-10 rounded-full bg-black flex items-center justify-center text-primary-foreground">
|
||||
<FontAwesomeIcon
|
||||
icon={faMedal}
|
||||
className="text-white h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">Finalist</h3>
|
||||
<p className="text-sm">
|
||||
36th National Student Science Week (PIMNAS)
|
||||
| Nov 2023
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
<div className="aspect-square w-10 rounded-full bg-black flex items-center justify-center text-primary-foreground">
|
||||
<FontAwesomeIcon
|
||||
icon={faMedal}
|
||||
className="text-white h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
3rd place (Bronze Medal)
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
FORKAFEST 3.0 Web Development competition |
|
||||
Jan 2023
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function GraduationCapIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z" />
|
||||
<path d="M22 10v6" />
|
||||
<path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
105
app/about/components/experience.jsx
Normal file
105
app/about/components/experience.jsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
export default function Experience() {
|
||||
return (
|
||||
<section className="grid gap-8 md:gap-12">
|
||||
<div className="relative after:absolute after:inset-y-2 after:w-1 after:bg-gray-700 after:left-[-4px] md:after:left-1 lg:after:left-2 xl:after:left-3 grid gap-8 md:gap-12 pl-6 md:pl-8">
|
||||
<div className="grid gap-4 relative">
|
||||
<div className="aspect-square w-5 bg-black rounded-full absolute left-0 translate-x-[-29.5px] z-10" />
|
||||
<div className="font-medium text-lg">2023 - Present</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl text-black">
|
||||
PT Hafdzamedia Teknologi Aplikasi
|
||||
</h3>
|
||||
<h4 className=" font-light text-md mb-4">
|
||||
Fullstack Web Developer | Part-time
|
||||
</h4>
|
||||
<p className="text-justify">
|
||||
Responsible for building and maintaining scalable
|
||||
web applications using the latest technologies.
|
||||
Collaborated with cross-functional teams to deliver
|
||||
high-quality solutions that meet business
|
||||
requirements.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-4 text-sm">
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
React
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Next.js
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Laravel
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
MySql
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 relative">
|
||||
<div className="aspect-square w-5 bg-black rounded-full absolute left-0 translate-x-[-29.5px] z-10" />
|
||||
<div className="font-medium text-lg">2024 - Present</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl text-black">
|
||||
Outlier
|
||||
</h3>
|
||||
<h4 className=" font-light text-md mb-4">
|
||||
AI Trainer | Freelance
|
||||
</h4>
|
||||
<p className="text-justify">
|
||||
Crafted and refined chatbot prompts using prompt
|
||||
engineering and RLHF techniques to improve the AI
|
||||
responses in English and Indonesian, leading to more
|
||||
accurate and contextually relevant interactions.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-4 text-sm">
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Generative AI
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Prompt Engineering
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Problem Solving
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 relative">
|
||||
<div className="aspect-square w-5 bg-black rounded-full absolute left-0 translate-x-[-29.5px] z-10" />
|
||||
<div className="font-medium text-lg">2023 - Present</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl text-black">
|
||||
Self-Employed
|
||||
</h3>
|
||||
<h4 className=" font-light text-md mb-4">
|
||||
Web Developer | Freelance
|
||||
</h4>
|
||||
<p className="text-justify">
|
||||
Specialized in crafting innovative solutions using
|
||||
Laravel and Next.js, enhancing client satisfaction
|
||||
and project efficiency. Contributed expertise in
|
||||
both frontend and backend development during
|
||||
collaborative team projects, leading to successful
|
||||
project completions
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-4 text-sm">
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
React
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Next.js
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
Laravel
|
||||
</div>
|
||||
<div className="bg-gray-300 text-black px-2 py-1 rounded-2xl">
|
||||
MySql
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
63
app/about/components/quote/quote.jsx
Normal file
63
app/about/components/quote/quote.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Quote.js
|
||||
import "./style.css";
|
||||
import { motion } from "framer-motion";
|
||||
import { useIntersectionObserver } from "./useIntersectionObserver";
|
||||
|
||||
export default function Quote() {
|
||||
const text1 = '"There are no limits to what you can accomplish'.split(" ");
|
||||
const text2 = 'except the limits you place on your own thinking."'.split(
|
||||
" "
|
||||
);
|
||||
const [ref, isIntersecting] = useIntersectionObserver();
|
||||
|
||||
return (
|
||||
<div ref={ref} className="text-center">
|
||||
<h3 className="text-[2rem]">
|
||||
{text1.map((word, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
filter: "blur(4px)",
|
||||
scale: 0.92,
|
||||
}}
|
||||
animate={{
|
||||
opacity: isIntersecting ? 1 : 0,
|
||||
filter: isIntersecting ? "blur(0px)" : "blur(4px)",
|
||||
scale: isIntersecting ? 1 : 0.92,
|
||||
}}
|
||||
transition={{
|
||||
delay: isIntersecting ? index * 0.1 : 0,
|
||||
duration: 0.5,
|
||||
}}>
|
||||
{word}{" "}
|
||||
</motion.span>
|
||||
))}
|
||||
</h3>
|
||||
<h3 className="text-xl">
|
||||
{text2.map((word, index) => (
|
||||
<motion.span
|
||||
key={index + text1.length}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
filter: "blur(4px)",
|
||||
scale: 0.92,
|
||||
}}
|
||||
animate={{
|
||||
opacity: isIntersecting ? 1 : 0,
|
||||
filter: isIntersecting ? "blur(0px)" : "blur(4px)",
|
||||
scale: isIntersecting ? 1 : 0.92,
|
||||
}}
|
||||
transition={{
|
||||
delay: isIntersecting
|
||||
? (text1.length + index) * 0.1
|
||||
: 0,
|
||||
duration: 0.5,
|
||||
}}>
|
||||
{word}{" "}
|
||||
</motion.span>
|
||||
))}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
app/about/components/quote/style.css
Normal file
45
app/about/components/quote/style.css
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
@keyframes scale {
|
||||
100% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-ms-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
-webkit-filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.q-wrap {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
filter: blur(4px);
|
||||
-webkit-filter: blur(4px);
|
||||
}
|
||||
|
||||
.q-wrap:nth-child(1) {
|
||||
animation: fade-in 0.8s 0.1 forwards cubic-bezier(0.11, 0, 0.5, 0);
|
||||
-webkit-animation: fade-in 0.8s 0.1 forwards cubic-bezier(0.11, 0, 0.5, 0);
|
||||
}
|
||||
|
||||
.q-wrap:nth-child(2) {
|
||||
animation: fade-in 0.8s 0.2 forwards cubic-bezier(0.11, 0, 0.5, 0);
|
||||
-webkit-animation: fade-in 0.8s 0.2 forwards cubic-bezier(0.11, 0, 0.5, 0);
|
||||
}
|
||||
|
||||
.q {
|
||||
text-align: center;
|
||||
transform: scale(0.92);
|
||||
-webkit-transform: scale(0.92);
|
||||
-moz-transform: scale(0.92);
|
||||
-ms-transform: scale(0.92);
|
||||
-o-transform: scale(0.92);
|
||||
animation: scale 3s forwards cubic-bezier(0.5, 1, 0.89, 1);
|
||||
-webkit-animation: scale 3s forwards cubic-bezier(0.5, 1, 0.89, 1);
|
||||
}
|
||||
32
app/about/components/quote/useIntersectionObserver.js
Normal file
32
app/about/components/quote/useIntersectionObserver.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// useIntersectionObserver.js
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export function useIntersectionObserver() {
|
||||
const [isIntersecting, setIsIntersecting] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
observer.unobserve(ref.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [ref, isIntersecting];
|
||||
}
|
||||
251
app/about/components/skills/skills.jsx
Normal file
251
app/about/components/skills/skills.jsx
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
export default function Skills() {
|
||||
return (
|
||||
<section className="grid gap-8 md:gap-12">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-3xl font-bold mt-3 text-black">
|
||||
Skills and Expertise
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-[800px] mx-auto">
|
||||
Explore some skills I'm proficient in to deliver high-quality
|
||||
solutions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<CodepenIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Web Development</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<WebhookIcon className="w-12 h-12" />
|
||||
<div className="font-medium">REST API</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ActivityIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Machine Learning</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<MobileIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Mobile Development</div>
|
||||
</div>
|
||||
{/* <div className="flex flex-col items-center gap-2">
|
||||
<CloudIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Cloud Computing</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<BarChartIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Data Analysis</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ComputerIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Cybersecurity</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<PackageIcon className="w-12 h-12" />
|
||||
<div className="font-medium">Product Management</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ActivityIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
function CodepenIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2" />
|
||||
<line x1="12" x2="12" y1="22" y2="15.5" />
|
||||
<polyline points="22 8.5 12 15.5 2 8.5" />
|
||||
<polyline points="2 15.5 12 8.5 22 15.5" />
|
||||
<line x1="12" x2="12" y1="2" y2="8.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function BarChartIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<line x1="12" x2="12" y1="20" y2="10" />
|
||||
<line x1="18" x2="18" y1="20" y2="4" />
|
||||
<line x1="6" x2="6" y1="20" y2="16" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ComputerIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<rect width="14" height="8" x="5" y="2" rx="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" />
|
||||
<path d="M6 18h2" />
|
||||
<path d="M12 18h6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PackageIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<path d="m7.5 4.27 9 5.15" />
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
||||
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||
<path d="M12 22V12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectorIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<path d="M5 7 3 5" />
|
||||
<path d="M9 6V3" />
|
||||
<path d="m13 7 2-2" />
|
||||
<circle cx="9" cy="13" r="3" />
|
||||
<path d="M11.83 12H20a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2.17" />
|
||||
<path d="M16 16h2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function TypeIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<polyline points="4 7 4 4 20 4 20 7" />
|
||||
<line x1="9" x2="15" y1="20" y2="20" />
|
||||
<line x1="12" x2="12" y1="4" y2="20" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WebhookIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<path d="M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2" />
|
||||
<path d="m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06" />
|
||||
<path d="m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
|
||||
<line x1="12" x2="12" y1="18" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
28
app/about/components/skills/stack.jsx
Normal file
28
app/about/components/skills/stack.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export default function Stack() {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-2xl md:text-xl font-normal mb-3 md:tracking-[.3rem] lg:tracking-[.5rem] uppercase ">
|
||||
Language & Framework
|
||||
</h2>
|
||||
<p className="text-gray-500 text-center tracking-widest md:px-5">
|
||||
<span className="text-black font-bold">HTML</span> |{" "}
|
||||
<span className="text-black font-bold">CSS</span> |{" "}
|
||||
<span className="text-black font-bold">Javascript</span> |{" "}
|
||||
<span className="text-black font-bold">Typescript</span> |{" "}
|
||||
<span className="text-black font-bold">React</span> |{" "}
|
||||
<span className="text-black font-bold">NextJS</span> |{" "}
|
||||
<span className="text-black font-bold">TailwindCSS</span> |{" "}
|
||||
<span className="text-black font-bold">Bootstrap</span> |{" "}
|
||||
<span className="text-black font-medium">NodeJS</span> |{" "}
|
||||
<span className="text-black font-medium">ExpressJS</span> |{" "}
|
||||
<span className="text-black font-bold">MySQL</span> |{" "}
|
||||
<span className="text-black font-bold">PostgreSQL</span> |{" "}
|
||||
<span className="text-black font-medium">MongoDB</span> |{" "}
|
||||
<span className="text-black font-medium">Firebase</span> |{" "}
|
||||
<span className="text-black font-medium">PHP</span> |{" "}
|
||||
<span className="text-black font-bold">Laravel</span> |{" "}
|
||||
<span className="text-black font-medium">Python</span>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
app/about/components/skills/tools.jsx
Normal file
26
app/about/components/skills/tools.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export default function Tools() {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-2xl md:text-xl font-normal mb-3 md:tracking-[.3rem] lg:tracking-[.5rem] uppercase">
|
||||
Tools
|
||||
</h2>
|
||||
<p className="text-gray-500 text-center tracking-wider md:px-">
|
||||
<span className="text-black font-bold">Visual Studio Code</span>{" "}
|
||||
| <span className="text-black font-medium">Jet Brains</span> |{" "}
|
||||
<span className="text-black font-bold">Git</span> |{" "}
|
||||
<span className="text-black font-bold">Github</span> |{" "}
|
||||
<span className="text-black font-medium">Figma</span> |{" "}
|
||||
<span className="text-black font-medium">Adobe Photoshop</span>{" "}
|
||||
|{" "}
|
||||
<span className="text-black font-medium">
|
||||
Adobe Premiere Pro
|
||||
</span>{" "}
|
||||
|{" "}
|
||||
<span className="text-black font-medium">
|
||||
Adobe After Effect
|
||||
</span>{" "}
|
||||
|{" "}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
app/about/components/spotify/animation.jsx
Normal file
25
app/about/components/spotify/animation.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { motion } from "framer-motion";
|
||||
|
||||
const PlayingAnimation = () => {
|
||||
return (
|
||||
<div className="relative flex justify-between w-4 h-8 space-x-1">
|
||||
<motion.span
|
||||
className="w-1 h-full bg-gray-500 rounded-full"
|
||||
animate={{ scaleY: [0.3, 1, 0.5, 0.75, 0.5] }}
|
||||
transition={{ duration: 2.2, repeat: Infinity }}
|
||||
/>
|
||||
<motion.span
|
||||
className="w-1 h-full bg-gray-500 rounded-full"
|
||||
animate={{ scaleY: [0.3, 1, 0.5, 0.75, 0.5] }}
|
||||
transition={{ duration: 2.2, repeat: Infinity, delay: -2 }}
|
||||
/>
|
||||
<motion.span
|
||||
className="w-1 h-full bg-gray-500 rounded-full"
|
||||
animate={{ scaleY: [0.3, 1, 0.5, 0.75, 0.5] }}
|
||||
transition={{ duration: 2.2, repeat: Infinity, delay: -3.7 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayingAnimation;
|
||||
80
app/about/components/spotify/card.jsx
Normal file
80
app/about/components/spotify/card.jsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import getNowPlayingItem from "./fetch";
|
||||
import PlayingAnimation from "./animation";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSpotify } from "@fortawesome/free-brands-svg-icons";
|
||||
import Image from "next/image";
|
||||
|
||||
const Card = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [result, setResult] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const results = await Promise.all([getNowPlayingItem()]);
|
||||
console.log(results);
|
||||
setResult(results[0]);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchData(); // Fetch data immediately on component mount
|
||||
|
||||
const intervalId = setInterval(fetchData, 60 * 1000); // ! Fetch data every minute: temporary
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex justify-center w-full">
|
||||
{loading ? (
|
||||
<div className="flex justify-center mb-8">
|
||||
{/* Replace this with your own loading spinner */}
|
||||
<div className="loader ease-linear rounded-full border-4 border-t-4 border-black h-12 w-12 mb-4"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative w-full mb-8 grid grid-cols-2 gap-2 border-2 border-gray-500 p-4 rounded-lg backdrop-filter backdrop-blur-lg bg-white bg-opacity-30">
|
||||
{result.isPlaying && (
|
||||
<Image
|
||||
src={result.albumImageUrl}
|
||||
alt="backgroundImage"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="z-0 opacity-20 absolute"
|
||||
/>
|
||||
)}
|
||||
<div className="z-10 flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpotify}
|
||||
className="text-black text-5xl me-5 md:me-1 xl:me-8"
|
||||
/>
|
||||
<p className="font-semibold me-5 md:me-1 xl:me-7">
|
||||
{result.isPlaying
|
||||
? "Now playing"
|
||||
: "Currently offline"}
|
||||
</p>
|
||||
{result.isPlaying && <PlayingAnimation />}
|
||||
</div>
|
||||
{result.isPlaying && (
|
||||
<div className="z-10 rounded-lg ms-5">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="overflow-hidden">
|
||||
<a
|
||||
href={result.songUrl}
|
||||
target="_blank"
|
||||
className="block font-semibold w-full truncate text-blue-500">
|
||||
{result.title}
|
||||
</a>
|
||||
<p className="truncate text-gray-500">
|
||||
{result.artist}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
||||
|
||||
53
app/about/components/spotify/fetch.js
Normal file
53
app/about/components/spotify/fetch.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import querystring from "querystring";
|
||||
const TOKEN_ENDPOINT = `https://accounts.spotify.com/api/token`;
|
||||
const NOW_PLAYING_ENDPOINT = `https://api.spotify.com/v1/me/player/currently-playing`;
|
||||
const client_id = process.env.NEXT_PUBLIC_SPOTIFY_CLIENT_ID;
|
||||
const client_secret = process.env.NEXT_PUBLIC_SPOTIFY_CLIENT_SECRET;
|
||||
const refresh_token = process.env.NEXT_PUBLIC_SPOTIFY_REFRESH_TOKEN;
|
||||
|
||||
const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
|
||||
const getAccessToken = async () => {
|
||||
const response = await fetch(TOKEN_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: querystring.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getNowPlaying = async () => {
|
||||
const { access_token } = await getAccessToken();
|
||||
return fetch(NOW_PLAYING_ENDPOINT, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default async function getNowPlayingItem() {
|
||||
const response = await getNowPlaying();
|
||||
if (response.status === 204 || response.status > 400) {
|
||||
return false;
|
||||
}
|
||||
const song = await response.json();
|
||||
const albumImageUrl = song.item.album.images[0].url;
|
||||
const artist = song.item.artists.map((_artist) => _artist.name).join(", ");
|
||||
const isPlaying = song.is_playing;
|
||||
const songUrl = song.item.external_urls.spotify;
|
||||
const title = song.item.name;
|
||||
|
||||
return {
|
||||
albumImageUrl,
|
||||
artist,
|
||||
isPlaying,
|
||||
songUrl,
|
||||
title,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue