mirror of
https://github.com/ihatenodejs/aidxnCC.git
synced 2025-04-24 13:55:57 +00:00
feat: improved locale selection (made button, use i18n)
This commit is contained in:
parent
bf4ed43f50
commit
e98a80666f
20
app/i18n.ts
Normal file
20
app/i18n.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
'en-US': {
|
||||
translation: require('../public/locales/en-US.json')
|
||||
}
|
||||
},
|
||||
fallbackLng: 'en-US',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n
|
@ -1,10 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect } from 'react'
|
||||
import './globals.css'
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import '../i18n'
|
||||
|
||||
config.autoAddCss = false
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { House, Link as LinkIcon, User, Phone, BookOpen, Music, Rss, X, Menu } from 'lucide-react';
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { House, Link as LinkIcon, User, Phone, BookOpen, Music, Rss, X, Menu, Globe, ChevronDown } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface NavItemProps {
|
||||
href: string;
|
||||
@ -19,6 +20,103 @@ const NavItem = ({ href, icon, children }: NavItemProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const languages = [
|
||||
{ code: 'en-US', name: 'English' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
|
||||
{languages.find(lang => lang.code === i18n.language)?.name || 'English'}
|
||||
{!isMobile && (
|
||||
<ChevronDown className={`w-4 h-4 ml-1 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={20} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{buttonContent}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`${
|
||||
isMobile
|
||||
? 'relative mt-1 w-full bg-gray-800 rounded-md shadow-lg'
|
||||
: 'absolute right-0 mt-2 w-48 bg-gray-800 rounded-md shadow-lg z-50'
|
||||
}`}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="language-menu"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`block w-full text-left px-4 py-2 text-sm ${
|
||||
i18n.language === lang.code
|
||||
? 'text-white bg-gray-700'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
||||
}`}
|
||||
role="menuitem"
|
||||
>
|
||||
{lang.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@ -41,7 +139,13 @@ export default function Header() {
|
||||
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
|
||||
<NavItem href="/music" icon={Music}>Music</NavItem>
|
||||
<NavItem href="https://disfunction.blog" icon={Rss}>Blog</NavItem>
|
||||
<div className="lg:hidden">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</ul>
|
||||
<div className="hidden lg:block">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
@ -1,31 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import { faPhone, faEnvelope } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub, faTelegram, faBluesky } from '@fortawesome/free-brands-svg-icons'
|
||||
import { Phone } from 'lucide-react'
|
||||
import ContactButton from '../objects/ContactButton'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function Contact() {
|
||||
const firstSectionStrings = ["I do a lot of things during the day and I'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 :(", "For the best chance of a response, please send me a message on Telegram. If you've made a pull request on one of my repos, I will most likely respond by the next day. If you've sent me an email, I will most likely respond within three days or less."]
|
||||
const secondSectionStrings = ["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'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.", "If you need to get in touch with me, please send me a message on Telegram or an email."]
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sections = [
|
||||
{ title: "I'm a busy person", texts: firstSectionStrings },
|
||||
{ title: "A note about calling and texting", texts: secondSectionStrings },
|
||||
]
|
||||
const contactButtonLabels = ["ihatenodejs", "@p0ntu5", "@aidxn.cc", "(802) 416-9516", "aidan@p0ntus.com"]
|
||||
const contactButtonHrefs = ["https://github.com/ihatenodejs", "https://t.me/p0ntu5", "https://bsky.app/profile/aidxn.cc", "tel:+18024169516", "mailto:aidan@p0ntus.com"]
|
||||
const contactButtonIcons = [faGithub, faTelegram, faBluesky, faPhone, faEnvelope]
|
||||
{
|
||||
title: t('contact.sections.busyPerson.title'),
|
||||
texts: t('contact.sections.busyPerson.texts', { returnObjects: true }) as string[]
|
||||
},
|
||||
{
|
||||
title: t('contact.sections.callingNote.title'),
|
||||
texts: t('contact.sections.callingNote.texts', { returnObjects: true }) as string[]
|
||||
}
|
||||
];
|
||||
|
||||
const contactButtonLabels = [
|
||||
t('contact.buttons.github'),
|
||||
t('contact.buttons.telegram'),
|
||||
t('contact.buttons.bluesky'),
|
||||
t('contact.buttons.phone'),
|
||||
t('contact.buttons.email')
|
||||
];
|
||||
|
||||
const contactButtonHrefs = [
|
||||
"https://github.com/ihatenodejs",
|
||||
"https://t.me/p0ntu5",
|
||||
"https://bsky.app/profile/aidxn.cc",
|
||||
"tel:+18024169516",
|
||||
"mailto:aidan@p0ntus.com"
|
||||
];
|
||||
|
||||
const contactButtonIcons = [faGithub, faTelegram, faBluesky, faPhone, faEnvelope];
|
||||
|
||||
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
|
||||
{t('contact.title')}
|
||||
</h1>
|
||||
<div className="p-6 space-y-4">
|
||||
{contactButtonLabels.map((label, index) => (
|
||||
<ContactButton key={index} label={label} href={contactButtonHrefs[index]} icon={contactButtonIcons[index]} className='mr-3'></ContactButton>
|
||||
))
|
||||
}
|
||||
<ContactButton
|
||||
key={index}
|
||||
label={label}
|
||||
href={contactButtonHrefs[index]}
|
||||
icon={contactButtonIcons[index]}
|
||||
className='mr-3'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{sections.map((section, sectionIndex) => (
|
||||
@ -35,8 +65,7 @@ export default function Contact() {
|
||||
<p key={index} className="text-gray-300 mb-4">{text}</p>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,17 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import Image from 'next/image'
|
||||
import Button from '../objects/Button'
|
||||
import LastPlayed from '@/components/widgets/LastPlayed';
|
||||
import LastPlayed from '@/components/widgets/LastPlayed'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Home() {
|
||||
const whoAmIStrings = ["Hey there! I'm Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.", "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", "When I'm not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch."]
|
||||
const whatIDoStrings = ["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.", "I host a few public services and websites on my VPS, most of which can be found on the \"Domains\" page with a short description.", "I'm most proud of LibreCloud/p0ntus mail, which is a cloud services provider that I self-host and maintain, free of charge.", "I frequently write and work on a website hosted on a public Linux server, known as a \"tilde\" You can check it out by clicking the link \"Tilde\" in the header, or \"what\" if you are still confused!"]
|
||||
const whereYouAreStrings = ["My website is my home, not my business. I am not here to brag about my accomplishments or plug my cool SaaS product. That's why I've made every effort to make this website as personal and fun as possible.", "From a technical perspective, you're being served this website by Vercel."]
|
||||
const mainStrings = [whoAmIStrings, whatIDoStrings, whereYouAreStrings]
|
||||
const contactString = "Feel free to reach out for collaborations or just a hello :)"
|
||||
const mainSections = ["Who I am", "What I do", "Where you are"]
|
||||
const sendMessage = "Send me a message"
|
||||
const myName = "Aidan"
|
||||
const myDescription = "SysAdmin, Developer, and Student"
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mainStrings: string[][] = [
|
||||
t('home.whoAmI', { returnObjects: true }) as string[],
|
||||
t('home.whatIDo', { returnObjects: true }) as string[],
|
||||
t('home.whereYouAre', { returnObjects: true }) as string[]
|
||||
];
|
||||
|
||||
const mainSections = [
|
||||
t('home.sections.whoIAm'),
|
||||
t('home.sections.whatIDo'),
|
||||
t('home.sections.whereYouAre')
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
@ -23,8 +31,8 @@ export default function Home() {
|
||||
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">{myName}</h1>
|
||||
<p className="text-gray-400 text-xl">{myDescription}</p>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">{t('home.profile.name')}</h1>
|
||||
<p className="text-gray-400 text-xl">{t('home.profile.description')}</p>
|
||||
</div>
|
||||
|
||||
<LastPlayed />
|
||||
@ -32,20 +40,28 @@ export default function Home() {
|
||||
{mainSections.map((section, secIndex) => (
|
||||
<section key={secIndex} id="about" className="mb-12">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{section}</h2>
|
||||
{mainStrings[secIndex].map((text, index) => (
|
||||
{mainStrings[secIndex].map((text: string, index: number) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
{text}
|
||||
{secIndex === 2 && index === 1 && (
|
||||
<>
|
||||
<Link href="https://nvd.nist.gov/vuln/detail/CVE-2025-29927" className="text-blue-400 hover:underline">
|
||||
CVE-2025-29927
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section id="contact">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{sendMessage}</h2>
|
||||
<p className="text-gray-300 mb-6">{contactString}</p>
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{t('home.contact.title')}</h2>
|
||||
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
|
||||
<Button
|
||||
href={'/contact'}
|
||||
label="Contact Me"
|
||||
label={t('home.contact.button')}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
20
i18n.ts
Normal file
20
i18n.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
'en-US': {
|
||||
translation: require('./public/locales/en-US.json')
|
||||
}
|
||||
},
|
||||
fallbackLng: 'en-US',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"home": {
|
||||
"intro": "SysAdmin, Developer, and Student",
|
||||
"whoami": "Hey there! I'm Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.",
|
||||
"interests": "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",
|
||||
"arch":"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch."
|
||||
}
|
||||
}
|
@ -14,11 +14,14 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"geist": "^1.3.1",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next": "^15.2.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
|
62
public/locales/en-US.json
Normal file
62
public/locales/en-US.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"home": {
|
||||
"whoAmI": [
|
||||
"Hey there! I'm Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.",
|
||||
"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",
|
||||
"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch."
|
||||
],
|
||||
"whatIDo": [
|
||||
"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.",
|
||||
"I host a few public services and websites on my VPS, most of which can be found on the \"Domains\" page with a short description.",
|
||||
"I'm most proud of LibreCloud/p0ntus mail, which is a cloud services provider that I self-host and maintain, free of charge.",
|
||||
"I frequently write and work on a website hosted on a public Linux server, known as a \"tilde\" You can check it out"
|
||||
],
|
||||
"whereYouAre": [
|
||||
"My website is my home, not my business. I am not here to brag about my accomplishments or plug my cool SaaS product. That's why I've made every effort to make this website as personal and fun as possible.",
|
||||
"This page is currently hosted on Cloudflare Workers, after what happened with "
|
||||
],
|
||||
"sections": {
|
||||
"whoIAm": "Who I am",
|
||||
"whatIDo": "What I do",
|
||||
"whereYouAre": "Where you are"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Send me a message",
|
||||
"description": "Feel free to reach out for collaborations or just a hello :)",
|
||||
"button": "Contact Me"
|
||||
},
|
||||
"profile": {
|
||||
"name": "Aidan",
|
||||
"description": "SysAdmin, Developer, and Student"
|
||||
},
|
||||
"miscWords": {
|
||||
"here": "here"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"sections": {
|
||||
"busyPerson": {
|
||||
"title": "I'm a busy person",
|
||||
"texts": [
|
||||
"I do a lot of things during the day and I'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 :(",
|
||||
"For the best chance of a response, please send me a message on Telegram. If you've made a pull request on one of my repos, I will most likely respond by the next day. If you've sent me an email, I will most likely respond within three days or less."
|
||||
]
|
||||
},
|
||||
"callingNote": {
|
||||
"title": "A note about calling and texting",
|
||||
"texts": [
|
||||
"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'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.",
|
||||
"If you need to get in touch with me, please send me a message on Telegram or an email."
|
||||
]
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"github": "ihatenodejs",
|
||||
"telegram": "@p0ntu5",
|
||||
"bluesky": "@aidxn.cc",
|
||||
"phone": "(802) 416-9516",
|
||||
"email": "aidan@p0ntus.com"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user