refactor: reorganize components and move files to appropriate directories

This commit is contained in:
Alvalen 2025-01-15 11:07:57 +07:00
parent b270dd1d21
commit 43a3d944ea
12 changed files with 7 additions and 7 deletions

View 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&rsquo;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>
);
}

View 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>
);
}

View 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>
);
}

View 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);
}

View 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];
}

View 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&apos;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>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View 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;

View 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;

View 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,
};
}