move components to root, remove analytics and speed insights, cleanup on manifesto, update music link text and tilde icon on header, minor home page improvements/tweaks

This commit is contained in:
Aidan 2025-02-01 21:44:25 -05:00
parent 1909a6d9fe
commit 1253a7e0a1
29 changed files with 65 additions and 59 deletions

14
components/Footer.tsx Normal file
View file

@ -0,0 +1,14 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHeart } from '@fortawesome/free-solid-svg-icons';
export default function Footer() {
return (
<footer className="bg-gray-800 py-6">
<div className="container mx-auto px-4 text-center">
<a href="https://git.pontusmail.org/aidan/aidxnCC" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
<FontAwesomeIcon icon={faHeart} className="text-md mr-1" /> This website is free, open source and in the public domain.
</a>
</div>
</footer>
)
}

54
components/Header.tsx Normal file
View file

@ -0,0 +1,54 @@
"use client";
import React, { useState } from 'react';
import Link from 'next/link';
import { House, Link as LinkIcon, User, Phone, BookOpen, Music, Users, X, Menu } from 'lucide-react';
interface NavItemProps {
href: string;
icon: React.ElementType;
children: React.ReactNode;
}
const NavItem = ({ href, icon, children }: NavItemProps) => (
<div className="nav-item">
<Link href={href} className="flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300">
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
{children}
</Link>
</div>
);
export default function Header() {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => setIsOpen(!isOpen);
return (
<header className="bg-gray-800 shadow-lg">
<nav className="container mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="text-gray-300 hover:text-white text-2xl font-bold transition-all duration-300 hover:glow">
aidxn.cc
</Link>
<button onClick={toggleMenu} className="md:hidden text-gray-300 focus:outline-none">
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
</button>
<ul className={`flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-4 absolute md:static bg-gray-800 md:bg-transparent w-full md:w-auto left-0 md:left-auto top-16 md:top-auto p-4 md:p-0 transition-all duration-300 ease-in-out ${isOpen ? 'flex' : 'hidden md:flex'}`}>
<NavItem href="/" icon={House}>Home</NavItem>
<NavItem href="/about" icon={User}>About</NavItem>
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
<NavItem href="/music" icon={Music}>Music</NavItem>
<div className="flex items-center">
<NavItem href="https://tilde.club/~lxu" icon={Users}>Tilde</NavItem>
<a href="https://tilde.wiki/Tildeverse" className="text-gray-300 hover:text-green-400 ml-1 text-sm" target="_blank" rel="noopener noreferrer">
<sup>what?</sup>
</a>
</div>
</ul>
</nav>
</header>
);
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import Link from 'next/link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
interface BackButtonProps {
href: string;
label?: string;
}
const BackButton: React.FC<BackButtonProps> = ({ href, label = 'Back' }) => {
return (
<Link
href={href}
className="inline-flex items-center px-4 py-2 mt-4 text-white bg-gray-800 rounded shadow-md transition-all duration-300 ease-in-out hover:bg-gray-700 hover:shadow-lg hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
aria-label={`Go back to ${label}`}
>
<FontAwesomeIcon icon={faArrowLeft} className="mr-2" />
{label}
</Link>
);
};
export default BackButton;

View file

@ -0,0 +1,20 @@
import React from 'react';
import Link from 'next/link';
interface MusicInfoButtonProps {
href: string;
label: string;
}
const MusicInfoButton: React.FC<MusicInfoButtonProps> = ({ href, label }) => {
return (
<Link
href={href}
className="inline-block bg-gray-800 text-white font-bold py-2 px-4 rounded shadow-md transition-all duration-300 ease-in-out hover:bg-gray-700 hover:shadow-lg hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
{label}
</Link>
);
};
export default MusicInfoButton;

View file

@ -0,0 +1,26 @@
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link';
interface ContactButtonProps {
href: string;
icon: IconDefinition;
label: string;
className?: string;
}
function ContactButton({ href, icon, label, className }: ContactButtonProps) {
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
className={`bg-gray-700 text-white px-4 py-2 rounded-full hover:bg-gray-600 transition-colors inline-flex items-center ${className}`}
>
<FontAwesomeIcon icon={icon} className="text-xl mr-2" />
{label}
</Link>
)
}
export default ContactButton;

View file

@ -0,0 +1,12 @@
import { Loader2 } from 'lucide-react';
const LoadingSpinner: React.FC = () => {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
<Loader2 className="w-12 h-12 text-white animate-spin" />
</div>
);
};
export default LoadingSpinner;

View file

@ -0,0 +1,33 @@
import React from 'react';
import Button from './Button';
interface TimePeriod {
title: string;
slug: string;
}
const timePeriods: TimePeriod[] = [
{ title: 'Late Summer 2024', slug: 'late-summer-2024' },
{ title: 'Early Summer 2024', slug: 'early-summer-2024' },
];
const MusicInfo: React.FC = () => {
return (
<div className="max-w-2xl mx-auto text-center text-gray-200">
{timePeriods.map((period) => (
<section key={period.slug} className="mb-12">
<h2 className="text-2xl font-semibold mb-4">{period.title}</h2>
<div className="flex justify-center">
<Button
href={`/time-periods/${period.slug}/what-was-going-on`}
label="WHAT WAS GOING ON"
/>
</div>
</section>
))}
</div>
);
};
export default MusicInfo;

View file

@ -0,0 +1,74 @@
"use client"
import { User } from 'lucide-react'
import FeaturedRepos from '../widgets/FeaturedRepos'
import Image from 'next/image'
import { useState } from 'react'
import Link from 'next/link'
export default function About() {
const [imageError, setImageError] = useState(false);
return (
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<User size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
About Me
</h1>
<div className="px-6 pt-6">
<p className="text-gray-300 mb-4">
Hey there! I&apos;m Aidan, a web developer and student, and this is my website. I&apos;m passionate about web development (although I&apos;m not great with design) and I love building things with Node.js and Express.
</p>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">Academics</h2>
<p className="text-gray-300 mb-4">
In terms of my academic background, I am currently pursuing a degree in computer science at SNHU. I really enjoy learning, though it depends on the subject. I am mostly self-taught when it comes to programming. I prefer this style of learning, especially with programming, as it lets me learn faster and apply creativity much more.
</p>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">Hobbies</h2>
<p className="text-gray-300 mb-4">
When I&apos;m not programming, I can typically be found installing another Linux distro on my laptop or flashing a new ROM to my phone. I am also a passionate writer and I like to write creatively in my free time.
</p>
<p className="text-gray-300">
I am almost always active on <Link href="https://git.pontusmail.org/" className="text-blue-400 hover:underline">my Gitea instance</Link> and GitHub and make daily contributions to several of my repositories. I am a big fan of open source software and public domain software (which most of my repos are licensed under). In fact, the website you&apos;re currently on is free and open source. It&apos;s even under the public domain!
</p>
</div>
<div className="mt-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">My Gitea/GitHub Contributions</h2>
<p className="text-gray-300 mb-4">Most of my repositories have migrated to <Link href="https://git.pontusmail.org/" className="text-blue-400 hover:underline">LibreCloud Git</Link>. My username is <Link href="https://git.pontusmail.org/aidan/" className="text-blue-400 hover:underline">aidan</Link>.</p>
<p className="text-gray-300 mb-4">You can find me on GitHub as <Link href="https://github.com/ihatenodejs/" className="text-blue-400 hover:underline">ihatenodejs</Link>.</p>
{!imageError && (
<div className="flex flex-col md:flex-row justify-center gap-4">
<Image
src="https://github-readme-stats.vercel.app/api?username=ihatenodejs&theme=dark&show_icons=true&hide_border=true&count_private=true"
alt="ihatenodejs's Stats"
width={500}
height={200}
className="w-full md:w-1/2"
onError={() => setImageError(true)}
loading="eager"
priority
unoptimized
/>
<Image
src="https://github-readme-stats.vercel.app/api/top-langs/?username=ihatenodejs&theme=dark&show_icons=true&hide_border=true&layout=compact"
alt="ihatenodejs's Top Languages"
width={500}
height={200}
className="w-full md:w-1/3"
onError={() => setImageError(true)}
loading="eager"
priority
unoptimized
/>
</div>
)}
</div>
<div className="mt-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Featured Projects</h2>
<p className="text-gray-300 mb-6">Here&apos;s just four of my top projects. Star and fork counts are manually updated and count both Gitea and GitHub.</p>
<FeaturedRepos />
</div>
</div>
)
}

View file

@ -0,0 +1,39 @@
import { faPhone, faEnvelope } from '@fortawesome/free-solid-svg-icons'
import { faGithub, faTelegram } from '@fortawesome/free-brands-svg-icons'
import { Phone } from 'lucide-react'
import ContactButton from '../objects/ContactButton'
export default function Contact() {
return (
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<Phone size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
Contact
</h1>
<div className="p-6 space-y-4">
<ContactButton href="https://github.com/ihatenodejs" icon={faGithub} label="ihatenodejs" className="mr-3" />
<ContactButton href="https://t.me/p0ntu5" icon={faTelegram} label="@p0ntu5" className="mr-3" />
<ContactButton href="tel:+18024169516" icon={faPhone} label="(802) 416-9516" className="mr-3" />
<ContactButton href="mailto:aidan@p0ntus.com" icon={faEnvelope} label="aidan@p0ntus.com" className="" />
</div>
<div className="p-6">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">I&apos;m a busy person</h2>
<p className="text-gray-300 mb-4">
I do a lot of things during the day and I&apos;m not always able to respond to messages right away. Please be patient and remember not to demand things from me... Somehow this is an issue for people :(
</p>
<p className="text-gray-300 mb-10">
For the best chance of a response, please send me a message on Telegram. If you&apos;ve made a pull request on one of my repos, I will most likely respond by the next day. If you&apos;ve sent me an email, I will most likely respond within three days or less.
</p>
<h2 className="text-2xl font-semibold mb-4 text-gray-200">A note about calling and texting</h2>
<p className="text-gray-300 mb-4">
I have a phone number listed above. Please do not call or text me unless you absolutely need to. I will likely not respond, or use an automated recording system to handle your call. No, I haven&apos;t provided you my real phone number. I may be able to respond to your call/text, just know this is not checked/used often.
</p>
<p className="text-gray-300 mb-4">
If you need to get in touch with me, please send me a message on Telegram or an email.
</p>
</div>
</div>
)
}

View file

@ -0,0 +1,23 @@
import { Link } from 'lucide-react'
import domains from '@/public/data/domains.json'
export default function About() {
return (
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<Link size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
My Domains
</h1>
<div className="p-6">
{domains.map(domain => (
<div key={domain.id} className="mb-4">
<h2 className="text-2xl font-semibold text-gray-200">{domain.domain}</h2>
<p className="text-gray-300">{domain.usage}</p>
</div>
))}
</div>
</div>
)
}

71
components/pages/Home.tsx Normal file
View file

@ -0,0 +1,71 @@
import Image from 'next/image'
import Button from '../objects/Button'
import LastPlayed from '@/components/widgets/LastPlayed';
export default function Home() {
return (
<div className="max-w-2xl mx-auto">
<div className="mb-12 text-center">
<Image
src="/ihatenodejs.jpg"
alt="My Profile Picture"
width={150}
height={150}
className="rounded-full mx-auto mb-6 border-4 border-gray-700"
/>
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">Aidan</h1>
<p className="text-gray-400 text-xl">SysAdmin, Developer, and Student</p>
</div>
<LastPlayed />
<section id="about" className="mb-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Who I am</h2>
<p className="text-gray-300 leading-relaxed">
Hey there! I&apos;m Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.
</p>
<p className="text-gray-300 leading-relaxed mt-2">
I am most interested in backend development and have experience with Node.js, Express, and Tailwind CSS. Despite my best efforts, I am no designer
</p>
<p className="text-gray-300 leading-relaxed mt-2">
When I&apos;m not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch.
</p>
</section>
<section id="about" className="mb-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">What I do</h2>
<p className="text-gray-300 leading-relaxed">
I am at my best when I am doing system administration, but I also enjoy working on web development projects. I enjoy contributing under open licenses more than anything. I have never felt much of a draw to profiting off my work.
</p>
<p className="text-gray-300 leading-relaxed mt-2">
I host a few public services and websites on my VPS, most of which can be found on the &quot;Domains&quot; page with a short description.
</p>
<p className="text-gray-300 leading-relaxed mt-2">
I&apos;m most proud of LibreCloud/p0ntus mail, which is a cloud services provider that I self-host and maintain, free of charge.
</p>
<p className="text-gray-300 leading-relaxed mt-2">
I frequently write and work on a website hosted on a public Linux server, known as a &quot;tilde.&quot; You can check it out by clicking the link &quot;Tilde&quot; in the header, or &quot;what?&quot; if you are still confused!
</p>
</section>
<section id="about" className="mb-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Where you are</h2>
<p className="text-gray-300 leading-relaxed">
My website is my home, not my business. I am not here to brag about my accomplishments or plug my cool SaaS product. That&apos;s why I&apos;ve made every effort to make this website as personal and fun as possible.
</p>
<p className="text-gray-300 leading-relaxed mt-2">
From a technical perspective, you&apos;re being served this website by Vercel.
</p>
</section>
<section id="contact">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Send me a message</h2>
<p className="text-gray-300 mb-6">Feel free to reach out for collaborations or just a hello :)</p>
<Button
href={'/contact'}
label="Contact Me"
/>
</section>
</div>
)
}

View file

@ -0,0 +1,67 @@
import { BookOpen } from 'lucide-react'
export default function About() {
return (
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<BookOpen size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
Manifesto
</h1>
<div className="px-6 pt-6">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
1. Empathy and Understanding
</h2>
<p className="text-gray-300 mb-4">
We live in a distant world. People I meet are from all over, which can be hard to understand for others. I aim to utilize my ability to connect by understanding and getting interested in people&apos;s lives. I pledge to:
</p>
<ul className="list-disc list-inside text-left text-gray-300 mt-8 mb-4">
<li>Listen deeply and genuinely</li>
<li>Suspend judgment and seek to understand</li>
<li>Recognize the humanity in every digital interaction</li>
</ul>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
2. Unconditional Sharing!
</h2>
<p className="text-gray-300 mb-4">
Information should be free and accessible to all. I will:
</p>
<ul className="list-disc list-inside text-left text-gray-300 mt-8 mb-4">
<li>Make all of my work free and accessible to all (e.g. public domain Wikipedia contributions)</li>
<li>Creating and sharing content for others benefit</li>
<li>Supporting open-source principles</li>
<li>Creating extensive documentation on all of my projects</li>
</ul>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
3. Genuine Human Connection
</h2>
<p className="text-gray-300 mb-4">
I aim to create a genuine human connection with all people I meet, regardless of who or where they are from.
</p>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
4. Privacy & Self-Hosted Services
</h2>
<p className="text-gray-300 mb-4">
In terms of my personal (some public) services, I commit to never selling, viewing or share personal information with third parties or myself. I will:
</p>
<ul className="list-disc list-inside text-left text-gray-300 mt-8 mb-4">
<li>Respect user data as a fundamental human right</li>
<li>Not implement tracking and/or monetization in my services
<ul className="list-disc list-inside text-left text-gray-300 mt-2 mb-4">
<li>Ensure user data is never used for profit</li>
</ul>
</li>
<li>Focus my services on being free and open</li>
<li>Suggest/support technologies that help privacy</li>
</ul>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
I Commit
</h2>
<p className="text-gray-300 mb-4">
I am not perfect, that&apos;s for sure, but I am committed. I promise to continuously learn, grow, and adapt to my environment, goals, purpose, and the people around me.
</p>
</div>
</div>
)
}

View file

@ -0,0 +1,27 @@
import React from 'react';
import BackButton from '../../../objects/BackButton';
const WhatWasGoingOnLateSummer2024: React.FC = () => {
return (
<div className="max-w-2xl mx-auto text-center">
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
What was going on during the start of summer 2024?
</h1>
<div className="px-6 pt-6">
<p className="text-gray-300 mb-4">
During Early Summer 2024, I was walking a ton in towns all across Massachusetts. During this time, I would listen to a <i>lot</i> of music. I regret not finding out about LastFM for so long... During this time, I was always happy, especially when I had music or a YouTube video playing. I would also call my friends often during this time.
</p>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">Context</h2>
<p className="text-gray-300 mb-4">
This summer was the one where I came back from my abusive treatment center. I was finally free from the place that had been holding me back for so long. So as you can imagine, I felt free as a bird.
</p>
<p className="text-gray-300 mb-4">
With this chance to explore, being in so many different towns, I really had a good time and made good memories, which I will not be writing about.
</p>
<BackButton href="/music" />
</div>
</div>
);
};
export default WhatWasGoingOnLateSummer2024;

View file

@ -0,0 +1,26 @@
import React from 'react';
import BackButton from '../../../objects/BackButton';
const WhatWasGoingOnLateSummer2024: React.FC = () => {
return (
<div className="max-w-2xl mx-auto text-center">
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
What was going on during the end of summer 2024?
</h1>
<div className="px-6 pt-6">
<p className="text-gray-300 mb-4">
During late summer 2024, my depression and the &quot;after effects&quot; of treatment really kicked in. I had quit going to my therapist as I didn&apos;t feel like they were doing much of anything for me. I am very happy to say that since I quit my therapist, I have been doing much better.
</p>
<p className="text-gray-300 mb-4">
At this time, the baseball season was over, so I was walking around much less. I was still listening to a lot of music and I started getting into less depressed songs. I was also starting to get into more &quot;normal&quot; music, which was an interesting phase (which I believe I&apos;m still in).
</p>
<p className="text-gray-300 mb-4">
A highlight of late summer 2024 was a vacation I took. This vacation has entire albums which remind me of it and I will always cherish those memories deeply.
</p>
<BackButton href="/music" />
</div>
</div>
);
};
export default WhatWasGoingOnLateSummer2024;

View file

@ -0,0 +1,29 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGitAlt, faGithub } from '@fortawesome/free-brands-svg-icons'
import { faStar, faCodeBranch } from '@fortawesome/free-solid-svg-icons'
import featuredProjects from '@/public/data/featured.json'
import Link from 'next/link'
export default function GitHubFeatured() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{featuredProjects.map((project) => (
<div key={project.id} className="bg-gray-800 p-6 rounded-lg shadow-md min-h-[200px] flex flex-col">
<div className="flex-1">
<h3 className="text-xl font-bold text-gray-100 mb-3">
<FontAwesomeIcon icon={project.github ? faGithub : faGitAlt} className="mr-2" /> {project.name}
</h3>
<p className="text-gray-300 flex-grow">{project.description}</p>
</div>
<div className="pt-4 border-t border-gray-700 flex justify-between items-center mt-auto">
<Link href={project.url} className="text-blue-400 hover:underline">View Repo</Link>
<div className="flex items-center text-gray-400">
<FontAwesomeIcon icon={faStar} className="mr-1" /> {project.stars}
<FontAwesomeIcon icon={faCodeBranch} className="ml-4 mr-1" /> {project.forks}
</div>
</div>
</div>
))}
</div>
)
}

View file

@ -0,0 +1,64 @@
"use client";
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLastfm } from '@fortawesome/free-brands-svg-icons'
import { faCompactDisc, faUser } from '@fortawesome/free-solid-svg-icons'
interface Track {
name: string;
artist: { '#text': string };
album: { '#text': string };
image: { '#text': string; size: string }[];
url: string;
'@attr'?: { nowplaying: string };
}
const LastPlayed: React.FC = () => {
const [track, setTrack] = useState<Track | null>(null);
const apiUrl = process.env.LASTFM_API_URL || 'https://lastfm-last-played.biancarosa.com.br/aidxn_/latest-song';
useEffect(() => {
fetch(apiUrl)
.then(response => response.json())
.then(data => setTrack(data.track))
.catch(error => console.error('Error fetching now playing:', error));
}, [apiUrl]);
if (!track) {
return (
<div className="max-w-2xl mx-auto mb-12">
<h2 className="text-2xl font-bold mb-4 text-gray-200">Last Played Song</h2>
<div className="flex justify-center items-center border border-gray-300 rounded-lg p-4 max-w-md mt-8">
<span className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full" role="status"></span>
</div>
</div>
);
}
return (
<div className="max-w-2xl mx-auto mb-12">
<h2 className="text-2xl font-bold mb-4 text-gray-200">Last Played Song</h2>
<div className="now-playing flex items-center border border-gray-300 rounded-lg p-4 max-w-md mt-8 bg-white bg-opacity-10 backdrop-filter backdrop-blur-lg">
<Image
src={track.image.find(img => img.size === 'large')?.['#text'] || '/placeholder.png'}
alt={track.name}
width={96}
height={96}
className="rounded-lg mr-4"
/>
<div>
<p className="font-bold">{track.name}</p>
<p><FontAwesomeIcon icon={faCompactDisc} className="mr-1" /> {track.album['#text']}</p>
<i><FontAwesomeIcon icon={faUser} className="mr-1" /> {track.artist['#text']}</i>
<a href={track.url} target="_blank" rel="noopener noreferrer" className="text-blue-500 flex items-center">
<FontAwesomeIcon icon={faLastfm} className="mr-2" /> View on Last.fm
</a>
</div>
</div>
</div>
);
};
export default LastPlayed;

View file

@ -0,0 +1,101 @@
"use client";
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { Play, SkipBack, SkipForward } from 'lucide-react';
import LoadingSpinner from '../objects/LoadingSpinner';
interface Song {
albumArt: string;
name: string;
artist: string;
duration: string;
link?: string;
}
interface Period {
timePeriod: string;
songs: Song[];
}
export default function Home() {
const [timePeriod, setTimePeriod] = useState('Early Summer 2024');
const [songs, setSongs] = useState<Song[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetch('/data/music.json')
.then(response => response.json())
.then((data: Period[]) => {
const selectedPeriod = data.find((period) => period.timePeriod === timePeriod);
const songsList = selectedPeriod ? selectedPeriod.songs : [];
setSongs(songsList);
setCurrentIndex(Math.floor(Math.random() * songsList.length));
setIsLoading(false);
})
.catch(error => {
console.error('Error fetching music data:', error);
setIsLoading(false);
});
}, [timePeriod]);
const handleNext = () => {
setCurrentIndex((currentIndex + 1) % songs.length);
};
const handlePrevious = () => {
setCurrentIndex((currentIndex - 1 + songs.length) % songs.length);
};
return (
<div className="max-w-2xl mx-auto">
<section id="music-carousel" className="mb-12">
<h2 className="text-3xl font-semibold mb-4 text-gray-200">Music By Time Period</h2>
<div className="mb-4 pb-4">
<label htmlFor="timePeriod" className="text-gray-300">Time Period:</label>
<select
id="timePeriod"
value={timePeriod}
onChange={(e) => setTimePeriod(e.target.value)}
className="ml-2 p-2 bg-gray-700 text-gray-300 rounded"
>
<option value="Early Summer 2024">Early Summer 2024</option>
</select>
</div>
{isLoading && <LoadingSpinner />}
{!isLoading && songs.length > 0 && (
<div className="relative">
<div className="text-center">
<Image
src={songs[currentIndex].albumArt}
alt={songs[currentIndex].name}
width={300}
height={300}
className="mx-auto mb-4 rounded-lg"
/>
<h3 className="text-2xl font-bold text-gray-100">{songs[currentIndex].name}</h3>
<p className="text-gray-300">{songs[currentIndex].artist}</p>
<p className="text-gray-300">{songs[currentIndex].duration}</p>
<div className="mt-4">
<button onClick={handlePrevious} className="mr-4 text-gray-300">
<SkipBack className="w-8 h-8" />
</button>
<button className="mr-4 text-gray-300" onClick={() => window.open(songs[currentIndex]?.link, '_blank')}>
<Play className="w-8 h-8" />
</button>
<button onClick={handleNext} className="text-gray-300">
<SkipForward className="w-8 h-8" />
</button>
</div>
</div>
</div>
)}
</section>
</div>
);
}