mirror of
				https://github.com/skidoodle/albert.lol.git
				synced 2025-02-15 06:09:15 +01:00 
			
		
		
		
	.
This commit is contained in:
		
							
								
								
									
										42
									
								
								src/components/Icon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/components/Icon.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import Link from 'next/link'; | ||||
| import toast from 'react-hot-toast'; | ||||
| import copy from 'copy-to-clipboard'; | ||||
|  | ||||
| type Icon = { | ||||
|   children: any; | ||||
|   reference: string; | ||||
|   copyValue?: boolean; | ||||
| }; | ||||
|  | ||||
| const notify = () => { | ||||
|   toast.remove(), | ||||
|     toast.success('Copied to clipboard', { | ||||
|       style: { | ||||
|         background: '#0f1012', | ||||
|         color: '#fff', | ||||
|         fontSize: '1em', | ||||
|       }, | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| export const Icon = ({ children, reference, copyValue }: Icon) => { | ||||
|   if (copyValue) { | ||||
|     return ( | ||||
|       <Link | ||||
|         href={''} | ||||
|         className={`cursor-pointer`} | ||||
|         onClick={() => { | ||||
|           notify(), copy(reference); | ||||
|         }} | ||||
|       > | ||||
|         {children} | ||||
|       </Link> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Link href={reference} target='_blank' className={'cursor-pointer'}> | ||||
|       {children} | ||||
|     </Link> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										30
									
								
								src/components/MainLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/components/MainLayout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { ThemeSwitcher } from '@/components/ThemeSwitcher'; | ||||
| import { NowPlayingCard } from '@/components/SpotifyCard'; | ||||
| import { SocialLayout } from '@/components/SocialLayout'; | ||||
| import { Toaster } from 'react-hot-toast'; | ||||
| import FadeIn from 'react-fade-in'; | ||||
|  | ||||
| export const MainLayout = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <ThemeSwitcher /> | ||||
|       <FadeIn> | ||||
|         <div className='ml-[10%] mr-[10%]'> | ||||
|           <div className='flex flex-col max-w-3xl mx-auto mb-16 mt-32'> | ||||
|             <h1 className='text-7xl font-bold'>albert</h1> | ||||
|             <p className='text-2xl text-gray-600 dark:text-gray-400 mt-2 font-semibold'> | ||||
|               {Math.floor( | ||||
|                 (new Date().getTime() - new Date('2004-07-22').getTime()) / | ||||
|                   (1000 * 60 * 60 * 24 * 365.25) | ||||
|               )} | ||||
|               -year-old system administrator | ||||
|             </p> | ||||
|             <SocialLayout /> | ||||
|             <NowPlayingCard /> | ||||
|             <Toaster position='bottom-center' reverseOrder={false} /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </FadeIn> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										21
									
								
								src/components/SocialLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/SocialLayout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { Icon } from '@/components/Icon'; | ||||
| import { socials } from '@/components/data/Socials'; | ||||
| import React from 'react'; | ||||
|  | ||||
| export const SocialLayout = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <div className='grid grid-flow-col w-48 mt-3 text-2xl space-x-8'> | ||||
|         {socials.map((social) => ( | ||||
|           <Icon | ||||
|             key={social.id} | ||||
|             reference={social.ref} | ||||
|             copyValue={social.copyValue} | ||||
|           > | ||||
|             {React.createElement(social.icon)} | ||||
|           </Icon> | ||||
|         ))} | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										48
									
								
								src/components/SpotifyCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/components/SpotifyCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import { truncate } from '@/utils/truncate'; | ||||
| import FadeIn from 'react-fade-in'; | ||||
| import Image from 'next/image'; | ||||
| import useSWR from 'swr'; | ||||
| import SongImage from '@/public/song.webp'; | ||||
| import Link from 'next/link'; | ||||
|  | ||||
| export const fetcher = (url: RequestInfo) => fetch(url).then((r) => r.json()); | ||||
|  | ||||
| export const NowPlayingCard = () => { | ||||
|   var { data: spotify } = useSWR('/api/spotify', fetcher, { | ||||
|     refreshInterval: 3000, | ||||
|     fallbackData: 'loading', | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <FadeIn> | ||||
|       <div className='mt-5 focus:outline-none transition duration-300 ease-in-out transform hover:scale-105 p-3 rounded-md border border-gray-800 shadow flex flex-row max-w-sm'> | ||||
|         {spotify.song ? ( | ||||
|           <Image | ||||
|             height={45} | ||||
|             width={45} | ||||
|             alt='Song cover art' | ||||
|             src={spotify.song?.image} | ||||
|           /> | ||||
|         ) : ( | ||||
|           <Image height={45} width={45} alt='Song cover art' src={SongImage} /> | ||||
|         )} | ||||
|         <div className='my-auto ml-4'> | ||||
|           <div className='font-semibold text-l sm:text-regular'> | ||||
|             Listening to{' '} | ||||
|             {spotify.song ? ( | ||||
|               <Link | ||||
|                 href={`${spotify.song.url}`} | ||||
|                 target='_blank' | ||||
|                 className='text-[#32a866]' | ||||
|               > | ||||
|                 {truncate(`${spotify.song.title}`, 20)} | ||||
|               </Link> | ||||
|             ) : ( | ||||
|               <span className='text-[#32a866]'>nothing</span> | ||||
|             )} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </FadeIn> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										35
									
								
								src/components/ThemeSwitcher.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/ThemeSwitcher.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { useTheme } from 'next-themes'; | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { BsSunFill, BsMoonFill } from 'react-icons/bs'; | ||||
|  | ||||
| export const ThemeSwitcher = () => { | ||||
|   const [mounted, setMounted] = useState(false); | ||||
|   const { theme, setTheme } = useTheme(); | ||||
|  | ||||
|   const toggle = () => { | ||||
|     if (theme === 'dark') { | ||||
|       setTheme('light'); | ||||
|     } else { | ||||
|       setTheme('dark'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => setMounted(true), []); | ||||
|  | ||||
|   if (!mounted) return null; | ||||
|  | ||||
|   return ( | ||||
|     <button | ||||
|       aria-label='Toggle Dark Mode' | ||||
|       type='button' | ||||
|       className='focus:outline-none bg-none xs:bg-gray-200 xs:dark:bg-gray-800 rounded-lg p-3 h-10 w-10 flex items-center justify-center ml-auto' | ||||
|       onClick={() => toggle()} | ||||
|     > | ||||
|       {theme === 'light' ? ( | ||||
|         <BsMoonFill style={{ fill: 'black' }} size={300} /> | ||||
|       ) : ( | ||||
|         <BsSunFill size={300} /> | ||||
|       )} | ||||
|     </button> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										45
									
								
								src/components/data/Socials.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/components/data/Socials.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import { IconType } from 'react-icons/lib'; | ||||
| import { | ||||
|   FaDiscord, | ||||
|   FaEnvelope, | ||||
|   FaGithub, | ||||
|   FaInstagram, | ||||
|   FaSteam, | ||||
| } from 'react-icons/fa'; | ||||
|  | ||||
| type Socials = { | ||||
|   id: number; | ||||
|   ref: string; | ||||
|   icon: IconType; | ||||
|   copyValue?: boolean; | ||||
| }; | ||||
|  | ||||
| export const socials: Array<Socials> = [ | ||||
|   { | ||||
|     id: 1, | ||||
|     ref: 'https://github.com/skidoodle', | ||||
|     icon: FaGithub, | ||||
|   }, | ||||
|   { | ||||
|     id: 2, | ||||
|     ref: 'https://steamcommunity.com/id/_albert', | ||||
|     icon: FaSteam, | ||||
|   }, | ||||
|   { | ||||
|     id: 3, | ||||
|     ref: 'contact@albert.lol', | ||||
|     icon: FaEnvelope, | ||||
|     copyValue: true, | ||||
|   }, | ||||
|   { | ||||
|     id: 4, | ||||
|     ref: 'https://www.instagram.com/albertadam_/', | ||||
|     icon: FaInstagram, | ||||
|   }, | ||||
|   { | ||||
|     id: 5, | ||||
|     ref: 'albert#8838', | ||||
|     icon: FaDiscord, | ||||
|     copyValue: true, | ||||
|   }, | ||||
| ]; | ||||
							
								
								
									
										19
									
								
								src/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { AppProps } from 'next/app'; | ||||
| import { Analytics } from '@vercel/analytics/react'; | ||||
| import { ThemeProvider } from 'next-themes'; | ||||
| import Head from 'next/head'; | ||||
| import '@/styles/globals.scss'; | ||||
|  | ||||
| export default function App({ Component, pageProps }: AppProps) { | ||||
|   return ( | ||||
|     <> | ||||
|       <Head> | ||||
|         <title>albert</title> | ||||
|       </Head> | ||||
|       <ThemeProvider> | ||||
|         <Component {...pageProps} /> | ||||
|       </ThemeProvider> | ||||
|       <Analytics /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/pages/_document.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/pages/_document.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { Html, Head, Main, NextScript } from 'next/document'; | ||||
|  | ||||
| export default function Document() { | ||||
|   return ( | ||||
|     <Html lang='en'> | ||||
|       <Head> | ||||
|         <link rel='preconnect' href='https://vitals.vercel-insights.com' /> | ||||
|         <meta name='title' content='albert' /> | ||||
|         <meta name='og:title' content='albert' /> | ||||
|         <meta name='description' content='system administrator' /> | ||||
|         <meta name='og:description' content='system administrator' /> | ||||
|         <meta name='theme-color' content='#000000' /> | ||||
|         <meta property='og:image' content='/favicon.ico' /> | ||||
|       </Head> | ||||
|       <body> | ||||
|         <Main /> | ||||
|         <NextScript /> | ||||
|       </body> | ||||
|     </Html> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/pages/api/spotify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/pages/api/spotify.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import type { NextApiRequest, NextApiResponse } from 'next'; | ||||
| import { SpotifyService } from '@/service/spotify'; | ||||
|  | ||||
| const { CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN } = process.env; | ||||
| const spotify = new SpotifyService(CLIENT_ID!, CLIENT_SECRET!, REFRESH_TOKEN!); | ||||
|  | ||||
| export default async function handler( | ||||
|   req: NextApiRequest, | ||||
|   res: NextApiResponse | ||||
| ) { | ||||
|   const song = await spotify.getCurrentSong(); | ||||
|   if (!song || !song.isPlaying) { | ||||
|     return res.status(200).json({ | ||||
|       nowplaying: false, | ||||
|     }); | ||||
|   } | ||||
|   res.status(200).json({ | ||||
|     nowplaying: true, | ||||
|     song: { | ||||
|       artist: song.artists.name, | ||||
|       title: song.title, | ||||
|       url: song.url, | ||||
|       image: song.album.image, | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { MainLayout } from '@/components/MainLayout'; | ||||
|  | ||||
| export default function Home() { | ||||
|   return ( | ||||
|     <> | ||||
|       <MainLayout /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										61
									
								
								src/service/spotify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/service/spotify.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| import { SongResult } from '@/utils/type' | ||||
| import { SongResultMap } from '@/utils/result' | ||||
|  | ||||
| import axios from 'axios' | ||||
|  | ||||
| export class SpotifyService { | ||||
|     private accessToken: string = '' | ||||
|     private clientId: string | ||||
|     private clientSecret: string | ||||
|     private refreshToken: string | ||||
|  | ||||
|     constructor(clientId: string, clientSecret: string, refreshToken: string) { | ||||
|         this.clientId = clientId | ||||
|         this.clientSecret = clientSecret | ||||
|         this.refreshToken = refreshToken | ||||
|     } | ||||
|  | ||||
|     private hasAccessToken(): boolean { | ||||
|         return this.accessToken !== '' | ||||
|     } | ||||
|  | ||||
|     private setAccessToken(token: string): void { | ||||
|         this.accessToken = token | ||||
|     } | ||||
|  | ||||
|     private async getAccessToken(): Promise<void> { | ||||
|         try { | ||||
|             const response = await axios({ url: 'https://accounts.spotify.com/api/token',  | ||||
|                 method: 'POST',  | ||||
|                 params: { | ||||
|                     client_id: this.clientId, | ||||
|                     client_secret: this.clientSecret, | ||||
|                     refresh_token: this.refreshToken, | ||||
|                     grant_type: 'refresh_token', | ||||
|                 } | ||||
|             }).then((res) => res.data) | ||||
|      | ||||
|             this.setAccessToken(response.access_token) | ||||
|         } catch { | ||||
|             throw new Error('Invalid credentials were given') | ||||
|         } | ||||
|     }  | ||||
|  | ||||
|     public async getCurrentSong(): Promise<SongResult>  { | ||||
|         try { | ||||
|             if(!this.hasAccessToken()) { | ||||
|                 await this.getAccessToken() | ||||
|             } | ||||
|  | ||||
|             const response = await axios({ url: 'https://api.spotify.com/v1/me/player/currently-playing',  | ||||
|                 method: 'GET',  | ||||
|                 headers: { | ||||
|                     'Authorization': 'Bearer ' + this.accessToken | ||||
|                 } | ||||
|             }).then((res) => res.data) | ||||
|             return SongResultMap.parseSong(response) | ||||
|         } catch { | ||||
|             return await this.getAccessToken() as any | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										38
									
								
								src/styles/globals.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/styles/globals.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| @import url('https://cdn.albert.lol/ClearSansRegular.woff'); | ||||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
|  | ||||
| @layer base { | ||||
|   html { | ||||
|     font-family: 'Clear Sans', sans-serif; | ||||
|     scrollbar-width: thin; | ||||
|     scrollbar-color: #8a58e0 transparent; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @layer components { | ||||
|   ::selection { | ||||
|     background-color: #8039e2; | ||||
|     color: #fff; | ||||
|   } | ||||
|  | ||||
|   ::-webkit-scrollbar { | ||||
|     width: 3px; | ||||
|   } | ||||
|  | ||||
|   ::-webkit-scrollbar-thumb { | ||||
|     background-color: #8a58e0; | ||||
|     border-radius: 10px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| :root { | ||||
|   --background: #fff; | ||||
|   --foreground: #000; | ||||
| } | ||||
|  | ||||
| [data-theme='dark'] { | ||||
|   --background: #000; | ||||
|   --foreground: #fff; | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/utils/mapper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/utils/mapper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export interface Mapper<T> {} | ||||
							
								
								
									
										25
									
								
								src/utils/result.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/utils/result.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { Mapper } from '@/utils/mapper' | ||||
| import { SongResult } from '@/utils/type' | ||||
|  | ||||
| export class SongResultMap implements Mapper<SongResult> { | ||||
|     public static parseSong(result: any): SongResult { | ||||
|         const { item } = result | ||||
|  | ||||
|         return { | ||||
|             progress: result.progress_ms, | ||||
|             title: item.name, | ||||
|             album: { | ||||
|                 name: item.album.name, | ||||
|                 image: item.album.images[1].url, | ||||
|                 release: item.album.release_date, | ||||
|             }, | ||||
|             artists: { | ||||
|                 name: item.artists.map((x: any) => x.name), | ||||
|                 url: item.artists.map((x: any) => x.external_urls.spotify) | ||||
|             }, | ||||
|             url: item.external_urls.spotify, | ||||
|             length: item.duration_ms, | ||||
|             isPlaying: result.is_playing | ||||
|         } | ||||
|       } | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/utils/truncate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/utils/truncate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export const truncate = (str: string, n: number) => | ||||
|   str.length > n ? str.slice(0, n - 1) + '...' : str; | ||||
							
								
								
									
										16
									
								
								src/utils/type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/utils/type.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| export type SongResult = { | ||||
|     progress: number | ||||
|     album: { | ||||
|         name: string | ||||
|         image: string | ||||
|         release: string   | ||||
|     }, | ||||
|     artists: { | ||||
|         name: string | ||||
|         url: string | ||||
|     }, | ||||
|     url: string | ||||
|     title: string | ||||
|     length: number | ||||
|     isPlaying: boolean | ||||
| } | ||||
		Reference in New Issue
	
	Block a user