This commit is contained in:
2025-04-17 22:39:01 -04:00
parent 0213e84e78
commit f56959cb7f
52 changed files with 2766 additions and 5111 deletions
+19 -36
View File
@@ -1,41 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
pnpm-debug.log*
lerna-debug.log*
# env files (can opt-in for committing if needed)
.env*
node_modules
dist
dist-ssr
*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+53 -2
View File
@@ -1,3 +1,54 @@
# Website
# React + TypeScript + Vite
This is my website, [alexav.gg](https://alexav.gg)!
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
},
});
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
"react-x": reactX,
"react-dom": reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs["recommended-typescript"].rules,
...reactDom.configs.recommended.rules,
},
});
```
+28
View File
@@ -0,0 +1,28 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);
-16
View File
@@ -1,16 +0,0 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Pathway+Gothic+One&display=swap" rel="stylesheet">
<title>alex</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-31
View File
@@ -1,31 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'storage.alexav.gg',
port: '',
pathname: '/content/**',
search: ''
},
{
protocol: 'https',
hostname: 'cdn.medal.tv',
port: '',
pathname: '/**',
search: ''
},
{
protocol: 'https',
hostname: 'lastfm.freetls.fastly.net',
port: '',
pathname: '/**',
search: ''
}
]
}
};
export default nextConfig;
+2214 -3930
View File
File diff suppressed because it is too large Load Diff
+22 -17
View File
@@ -1,28 +1,33 @@
{
"name": "website",
"version": "0.1.0",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"next": "15.1.6",
"next-plausible": "^3.12.4",
"@tailwindcss/vite": "^4.1.4",
"lucide-react": "^0.488.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"@eslint/js": "^9.22.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.26.1",
"vite": "^6.3.1"
}
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

-4
View File
@@ -1,4 +0,0 @@
# allow all crawlers idc
User-Agent: *
Disallow:
-20
View File
@@ -1,20 +0,0 @@
{
"name": "Alex Frantz",
"short_name": "Alex Frantz",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": ".",
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone"
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+212
View File
@@ -0,0 +1,212 @@
import React, { useState, useEffect } from "react";
import { Flashlight, FlashlightOff, Twitter, Github } from 'lucide-react'
import CordycepsImage from './assets/cordyceps.png'
import MusicComponent from "./components/MusicComponent";
import TVComponent from "./components/TVComponent";
export default function MouseFlashlight() {
const [pointerPosition, setPointerPosition] = useState({ x: 500, y: 500 });
const [isActive, setIsActive] = useState(false);
const [isTouchDevice, setIsTouchDevice] = useState(false);
const [isFlashlightEnabled, setIsFlashlightEnabled] = useState(false);
// Detect touch device on component mount
useEffect(() => {
const isTouchCapable =
"ontouchstart" in window ||
navigator.maxTouchPoints > 0;
setIsTouchDevice(isTouchCapable);
}, []);
// Handle mouse events for desktop
const handleMouseMove = (e: React.MouseEvent) => {
setPointerPosition({ x: e.clientX, y: e.clientY });
setIsActive(true);
};
const handleMouseLeave = () => {
setIsActive(false);
};
// Handle touch events for mobile
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches && e.touches[0]) {
e.preventDefault(); // Prevent scrolling while touching
const touch = e.touches[0];
setPointerPosition({ x: touch.clientX, y: touch.clientY });
setIsActive(true);
}
};
const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches && e.touches[0]) {
const touch = e.touches[0];
setPointerPosition({ x: touch.clientX, y: touch.clientY });
setIsActive(true);
}
};
const handleTouchEnd = () => {
// On mobile, we'll keep the last position visible for a moment
setTimeout(() => {
setIsActive(false);
}, 1000);
};
useEffect(() => {
// Define window event handlers with correct types
const windowMouseMove = (e: MouseEvent) => {
setPointerPosition({ x: e.clientX, y: e.clientY });
setIsActive(true);
};
const windowMouseLeave = () => {
setIsActive(false);
};
const windowTouchStart = (e: TouchEvent) => {
if (e.touches && e.touches[0]) {
const touch = e.touches[0];
setPointerPosition({ x: touch.clientX, y: touch.clientY });
setIsActive(true);
}
};
const windowTouchMove = (e: TouchEvent) => {
if (e.touches && e.touches[0]) {
e.preventDefault?.(); // Optional chaining in case preventDefault isn't available
const touch = e.touches[0];
setPointerPosition({ x: touch.clientX, y: touch.clientY });
setIsActive(true);
}
};
const windowTouchEnd = () => {
// On mobile, we'll keep the last position visible for a moment
setTimeout(() => {
setIsActive(false);
}, 1000);
};
if (isFlashlightEnabled) {
// Add desktop event listeners
window.addEventListener("mousemove", windowMouseMove);
window.addEventListener("mouseleave", windowMouseLeave);
// Add mobile event listeners
window.addEventListener("touchstart", windowTouchStart, { passive: false });
window.addEventListener("touchmove", windowTouchMove, { passive: false });
window.addEventListener("touchend", windowTouchEnd);
} else {
// Remove desktop event listeners
window.removeEventListener("mousemove", windowMouseMove);
window.removeEventListener("mouseleave", windowMouseLeave);
// Remove mobile event listeners
window.removeEventListener("touchstart", windowTouchStart);
window.removeEventListener("touchmove", windowTouchMove);
window.removeEventListener("touchend", windowTouchEnd);
}
// Cleanup when component unmounts
return () => {
window.removeEventListener("mousemove", windowMouseMove);
window.removeEventListener("mouseleave", windowMouseLeave);
window.removeEventListener("touchstart", windowTouchStart);
window.removeEventListener("touchmove", windowTouchMove);
window.removeEventListener("touchend", windowTouchEnd);
};
}, [isFlashlightEnabled]);
return (
<div
className="relative h-screen w-full overflow-hidden bg-black text-white"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div className="min-h-screen flex flex-col justify-center items-center mx-auto">
<div className="border border-white border-solid p-2 rounded-lg w-[500px] mb-4">
<div className="flex items-center justify-between">
<div className="ml-2" onClick={() => setIsFlashlightEnabled(!isFlashlightEnabled)}>
{
isFlashlightEnabled ? <Flashlight /> : <FlashlightOff />
}
</div>
<h1 className="text-2xl font-bold">welcome !</h1>
</div>
</div>
<div className="border border-white border-solid p-2 rounded-lg w-[500px]">
<div className="grid grid-cols-2">
<div>
<div className="p-4">
<h1 className="text-2xl font-bold italic opacity-95 inline">alex, 17</h1>
<img src="https://storage.alexav.gg/content/picturev2.jpg" width="150" className="rounded-md" alt="Profile" />
<div className="pt-2">
<MusicComponent />
<a href="https://last.fm/user/lulawex" target="_blank" className="hover:underline w-fit opacity-50">
powered by last.fm
</a>
</div>
</div>
</div>
<div className="p-4">
<h1 className="font-bold text-2xl italic">what i do</h1>
<p>
qa @ <a href="https://medal.tv" target="_blank" rel="noopener noreferrer" className="hover:underline">medal.tv</a>, game, code
</p>
<h1 className="font-bold text-2xl italic">obsessed with</h1>
<p>
tlou, twd, rdr
</p>
<h1 className="font-bold text-2xl italic">watching</h1>
<TVComponent />
<h1 className="font-bold text-2xl italic">socials</h1>
<div className="space-x-2">
<a href="https://twitter.com/carlgrimesdupe" target="_blank" rel="noopener noreferrer">
<Twitter className="inline" />
</a>
<a href="https://github.com/azpha" target="_blank" rel="noopener noreferrer">
<Github className="inline" />
</a>
</div>
</div>
</div>
</div>
</div>
{isFlashlightEnabled && (
<>
<div
className={`absolute inset-0 bg-black pointer-events-none transition-opacity duration-300 ${
isActive ? "opacity-95" : "opacity-0"
}`}
style={{
maskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, transparent 0%, black 100%)`,
WebkitMaskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, transparent 0%, black 100%)`,
}}
/>
<div
className="absolute inset-0 pointer-events-none z-20"
style={{
maskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, black 0%, transparent 100%)`,
WebkitMaskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, black 0%, transparent 100%)`,
}}
>
<img src={CordycepsImage} className="absolute bottom-0" alt="Cordyceps" />
</div>
<audio loop autoPlay={true} src={"/clicker-sounds.mp3"} />
</>
)}
</div>
);
}
-27
View File
@@ -1,27 +0,0 @@
'use server'
export async function submitContact(formData: FormData) {
try {
const name = formData.get('name')
const email = formData.get('email')
const subject = formData.get('subject')
const message = formData.get('message')
const state = await fetch("https://api.alexav.gg/v4/contact", {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
name,
email,
subject,
message
})
})
return { success: state.ok }
} catch (e) {
return { success: false, error: (e as Error).message }
}
}
-21
View File
@@ -1,21 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
-68
View File
@@ -1,68 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import PlausibleProvider from "next-plausible";
import "./globals.css";
// components
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Alex Frantz",
description: "I am Alex Frantz, a junior developer working primarily as QA at Medal.tv. I do JavaScript and Python alongside learning Java and more.",
keywords: "alex frantz, frantz, alex, qa, javascript, html, react, python, java, alexavier frantz, alexavier, alexav, alexfrantz",
openGraph: {
title: "Alex Frantz",
url: "https://alexav.gg",
description: "I am Alex Frantz, a junior developer working primarily as QA at Medal.tv. I do JavaScript and Python alongside learning Java and more."
},
appleWebApp: {
title: "Alex Frantz",
startupImage: "/apple-touch-icon.png"
},
icons: [
{
rel: "icon",
type: "image/png",
sizes: "32x32",
url: "/favicon-32x32.png"
},
{
rel: "icon",
type: "image/png",
sizes: "16x16",
url: "/favicon-16x16.png"
}
],
manifest: "/site.webmanifest"
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<PlausibleProvider domain={"alexav.gg"} customDomain={'https://metrics.alexav.gg'}>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Header />
{children}
<Footer />
</body>
</html>
</PlausibleProvider>
);
}
-42
View File
@@ -1,42 +0,0 @@
import Link from "next/link";
import CustomButton from "@/components/CustomButton";
export default function Links() {
return (
<div className="min-h-screen bg-black flex justify-center items-center text-black">
<div className="bg-white rounded-lg p-4 text-center mb-20">
<Link href="/">
<h1 className="hover:underline font-bold text-2xl text-center">Alex Frantz</h1>
</Link>
<p>QA @ <a href="https://medal.tv" className="hover:underline" target="_blank">Medal.tv</a></p>
<hr className="my-2 mx-auto border-black" />
<div className="space-y-2">
<CustomButton
to="https://medal.tv/users/215577"
label="Medal"
/>
<CustomButton
to="https://twitter.com/carlgrimesdupe"
label="Twitter"
/>
<CustomButton
to="https://bsky.app/profile/alexav.gg"
label="Bluesky"
/>
<CustomButton
to="https://github.com/azpha"
label="GitHub"
/>
<CustomButton
to="https://last.fm/user/lulawex"
label="Last.fm"
/>
<CustomButton
to="https://linkedin.com/in/thatalex"
label="LinkedIn"
/>
</div>
</div>
</div>
)
}
-109
View File
@@ -1,109 +0,0 @@
import Image from 'next/image';
import { Twitter, Bluesky, Medal, GitHub, LinkedIn } from '@/components/Icons'
import ProjectComponent from '@/components/ProjectComponent';
import ContactForm from '@/components/ContactForm';
// data components
import RecentWatch from "@/components/DataComponents/RecentWatch";
import RecentListen from "@/components/DataComponents/RecentListen";
import RecentGame from "@/components/DataComponents/RecentGame";
// import RecentClip from "@/components/DataComponents/RecentClip";
import RecentBook from "@/components/DataComponents/RecentBook";
export default function Home() {
return (
<div className="select-none">
<section id="about">
<div className="mx-auto text-center p-10">
<div className="mx-auto grid gap-4 grid-cols-1 lg:grid-cols-2 lg:w-1/2">
<div className="flex justify-center">
<Image priority className="rounded-full" src={"https://storage.alexav.gg/content/picture.jpg"} alt="Profile Picture" width={250} height={250} />
</div>
<div className="text-center lg:text-left lg:my-10 w-full">
<h1 className="text-4xl font-bold">Hi, I&apos;m Alex</h1>
<p>I&apos;m a junior developer primarily working as QA, but sometimes writing questionable code at <a className="hover:underline text-yellow-400" href="https://medal.tv" target="_blank">Medal.tv</a></p>
<div className="block my-2 space-x-2">
<a href="https://twitter.com/carlgrimesdupe" target="_blank">
<Twitter />
</a>
<a href="https://bsky.app/profile/alexav.gg" target="_blank">
<Bluesky />
</a>
<a href="https://medal.tv/users/215577" target="_blank">
<Medal />
</a>
<a href="https://github.com/azpha/website" target="_blank">
<GitHub />
</a>
<a href="https://linkedin.com/in/thatalex" target="_blank">
<LinkedIn />
</a>
<a href="/links" className="opacity-40 text-sm">
<p className="underline">and more..</p>
</a>
</div>
</div>
</div>
</div>
</section>
<section id="projects">
<div className="bg-zinc-900 py-8">
<h1 className="pb-8 text-3xl text-center font-bold">My Projects</h1>
<div className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full md:w-[70%] lg:w-[50%] mx-auto">
<ProjectComponent
name="medaltv-wrapper"
description="A JavaScript wrapper for the Medal.tv developer API"
urlTo="https://github.com/azpha/medaltv-wrapper"
tags={["JavaScript"]}
/>
<ProjectComponent
name="splitstat"
description="A Discord bot for SplitStat: Arena Warfare statistics"
urlTo="https://github.com/azpha/splitstat"
tags={["JavaScript", "DiscordJS"]}
/>
<ProjectComponent
name="steam-deck-file-sync"
description="Sync clips from Steam Deck"
urlTo="https://github.com/azpha/steam-deck-file-sync"
tags={["Python", "Flask"]}
/>
<ProjectComponent
name="subscription-tracker"
description="Track upcoming subscriptions"
urlTo="https://github.com/azpha/subscription-tracker"
tags={["JavaScript", "ReactJS", "Vite"]}
/>
</div>
</div>
</div>
</section>
<section id="data">
<div className="py-8">
<h1 className="pb-8 text-3xl text-center font-bold">My Data</h1>
<div className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full md:w-[70%] lg:w-[50%] mx-auto">
<RecentWatch />
<RecentListen />
<RecentGame />
{/* <RecentClip /> */}
<RecentBook />
</div>
</div>
</div>
</section>
<section id="contact">
<div className="bg-zinc-900 py-8">
<ContactForm />
</div>
</section>
</div>
);
}
-26
View File
@@ -1,26 +0,0 @@
export default function Privacy() {
return (
<div className="my-6 text-left w-full lg:w-1/4 mx-auto">
<h1 className="text-2xl font-bold">Privacy</h1>
<h1>This site doesn&apos;t collect anything on you. No cookies (although tasty), no ominous advertisements, nothing.</h1>
<h1 className="my-2 font-bold text-2xl">Plausible</h1>
<h1>I use <a className="underline" href="https://plausible.io" target="_blank">Plausible analytics</a> to track traffic anonymously. Plausible;</h1>
<ul className="list-disc list-inside">
<li>does not collect personal identifying information</li>
<li className="underline"><a href="https://plausible.io/privacy-focused-web-analytics" target="_blank">is dedicated to privacy</a></li>
<li>compliant with the GDPR, CCPA and PECR data protection laws</li>
</ul>
<h1>I use a self-hosted instance in the US that doesn&apos;t use cookies.</h1>
<h1 className="my-2 font-bold text-2xl">Cloudflare</h1>
<h1>I use <a className="underline" href="https://pages.cloudflare.com" target="_blank">Cloudflare Pages</a> for this site. Most of my sites
are proxied through Cloudflare as well. You can view their privacy policy <a className="underline" href="https://cloudflare.com/privacy" target="_blank">here</a>.</h1>
<h1 className="my-2 font-bold text-2xl">Email</h1>
<h1>When you submit a contact request on my page, it is sent to Discord via a webhook & my own API.</h1>
<h1>This will be shown to me in an embed. I will never receive or process anything but what you provide.</h1>
<h1>You can learn more about Discord&apos;s dedication to privacy <a className='underline' href="https://discord.com/privacy" target="_blank">here</a>.</h1>
</div>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+5
View File
@@ -0,0 +1,5 @@
@import "tailwindcss";
body {
font-family: 'Pathway Gothic One', sans-serif;
}
-97
View File
@@ -1,97 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import InputBox from './InputBox';
import { submitContact } from '@/actions/contact';
export default function ContactForm() {
const [ error, setError ] = useState<string>("");
const [ success, setSuccess ] = useState<boolean>(false);
const [ pending, setPending ] = useState<boolean>(false);
const handleSubmission = async (formData: FormData) => {
setPending(true)
try {
const result = await submitContact(formData)
if (result.error || !result.success) {
setError('An unexpected error occurred')
} else setSuccess(true)
const form = document.getElementById('contact-form') as HTMLFormElement
form.reset()
} catch (e) {
console.error(e)
setError('An unexpected error occurred')
} finally {
setPending(false)
}
}
useEffect(() => {
setTimeout(() => {
setError("")
}, 6000)
}, [error]);
useEffect(() => {
setTimeout(() => {
setSuccess(false)
}, 6000)
}, [success]);
return (
<div className="rounded-lg p-2 w-full md:w-[50%] mx-auto">
<h1 className="text-2xl font-bold text-center pb-8">Contact Me</h1>
<form id="contact-form" action={handleSubmission} className="space-y-2 text-center">
<InputBox
isText={true}
name="name"
placeholder="Your name.."
disabled={pending}
/>
<InputBox
name="email"
type="email"
isText={true}
placeholder="your@supercool.email"
disabled={pending}
/>
<InputBox
isText={true}
name="subject"
placeholder="Subject.."
disabled={pending}
/>
<InputBox
isText={false}
name="message"
placeholder="Your message here.."
disabled={pending}
/>
<div className="block text-center">
<button
type="submit"
className="bg-white w-full text-black font-bold rounded-lg max-w-fit p-2"
>
Submit
</button>
{
error && (
<p className="text-red-400 font-bold text-center pt-2">{error}</p>
)
}
{
success && (
<p className="font-bold text-green-500 text-center pt-2">Success! Your message has been received</p>
)
}
</div>
</form>
</div>
)
}
-24
View File
@@ -1,24 +0,0 @@
import React from "react"
import Link from "next/link"
export default function CustomButton({
to,
label
}: {
to: string,
label: string
}) {
return (
<div>
<Link href={to} target="_blank">
<div className={`bg-black text-white text-lg font-semibold text-left p-2 w-52 rounded-lg`}>
<div className="px-1 space-x-2">
<div className="inline">
{label}
</div>
</div>
</div>
</Link>
</div>
)
}
-36
View File
@@ -1,36 +0,0 @@
import Image from "next/image"
export default async function DataComponent({
url,
header,
projectHeader,
projectSubheader,
projectImage
}: {
url: string,
header: string,
projectHeader: string,
projectSubheader: string,
projectImage?: string
}) {
return (
<a href={url} target="_blank">
<div className="bg-white rounded-lg text-black p-2">
<h1 className="text-center font-semibold">{header}</h1>
<div className="block">
<div className="flex items-center">
{
projectImage && <Image alt="Project Image" src={projectImage} width={100} height={100} className="w-[80px] h-[50px] object-cover" />
}
<div className="flex flex-col mx-2 min-w-0 flex-grow">
<h1 className="font-bold text-left truncate">{projectHeader}</h1>
<h1 className="text-left truncate">{projectSubheader}</h1>
</div>
</div>
</div>
</div>
</a>
)
}
@@ -1,46 +0,0 @@
import DataComponent from '../DataComponent';
import type { ContentTrackerItem } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/tracker/latest/book", {
method: 'get',
next: { revalidate: 60 }
}).then((res) => res.json())
return data
}
export default async function RecentBook() {
const data = await fetchData() as ContentTrackerItem
const getDateString = (startedOn: Date, finished: boolean) => {
if (startedOn && !finished) {
const startedOnDate = new Date(startedOn);
return "Started on " + startedOnDate.toLocaleDateString();
} else if (finished) {
return "Finished!"
} else {
return "Just Added"
}
}
if (data) {
return (
<DataComponent
header={"What I'm reading"}
projectHeader={data.title}
projectSubheader={getDateString(data.startedOn, data.finished)}
url={"https://tracker.alexav.gg/?type=book&id=" + data.id}
projectImage={data.image}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -1,47 +0,0 @@
import DataComponent from '../DataComponent';
import type { MedalClip } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/social/medal", {
method: 'get',
next: { revalidate: 60 }
}).then((res) => res.json())
return data[0]
}
export default async function RecentClip() {
const data = await fetchData() as MedalClip
const translateToWebsiteUrl = (url: string) => {
return url.replace('/clip/', "/clips/")
}
// i'm a neat man what can i say
const likeOrLikes = (likes: number) => {
return (likes > 1 && likes !== 0) ? "likes" : "like"
}
const viewOrViews = (views: number) => {
return (views > 1 && views !== 0) ? "views" : "view"
}
if (data) {
return (
<DataComponent
url={translateToWebsiteUrl(data.directClipUrl)}
header={"Clips I've taken"}
projectHeader={data.contentTitle}
projectSubheader={`${data.contentLikes} ${likeOrLikes(data.contentLikes)} | ${data.contentViews} ${viewOrViews(data.contentViews)}`}
projectImage={data.contentThumbnail}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -1,44 +0,0 @@
import DataComponent from '../DataComponent';
import type { GameData } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/game", {
method: 'get',
next: { revalidate: 10 }
}).then((res) => res.json())
return data
}
export default async function RecentGame() {
const data = await fetchData() as GameData
const getTime = (time: number) => {
const date = new Date(time*1000);
return `${date.toLocaleDateString()} at ${date.toLocaleTimeString()}`
}
if (data) {
return (
<DataComponent
url={"https://medal.tv/u/alexav"}
header={"Games I'm playing"}
projectHeader={data.name}
projectSubheader={
data.finished ? `Last played on ${getTime(data.startedOn)}`
: `Started on ${getTime(data.startedOn)}`
}
projectImage={"https://storage.alexav.gg/content/933d5523-5e20-4040-9fe9-b73552eac8ab.png"}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -1,36 +0,0 @@
import DataComponent from '../DataComponent';
import type { MusicData } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/social/music", {
method: 'get',
next: { revalidate: 60 }
}).then((res) => res.json())
return data.tracks[0]
}
export default async function RecentListen() {
const data = await fetchData() as MusicData
if (data) {
return (
<DataComponent
url={"https://last.fm/user/lulawex"}
header={"Music I'm listening to"}
projectHeader={data.name}
projectSubheader={data.artist["#text"]}
projectImage={data.image.filter((v) => v.size === "medium")[0]["#text"]}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -1,45 +0,0 @@
import DataComponent from '../DataComponent';
import type { WatchData } from '@/utils/types';
const fetchData = async () => {
const data = await fetch("https://api.alexav.gg/v4/tracker/latest/tv", {
method: 'get',
next: { revalidate: 60 }
});
return await data.json()
}
export default async function RecentWatch() {
const data = await fetchData() as WatchData
const getSubheaderText = () => {
if (data?.data.finished) {
return "Finished!"
} else if (data?.data.startedOn) {
return "Last update on " + new Date(data.data.updatedAt).toLocaleDateString()
} else {
return "Just Added"
}
}
if (data) {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={data.data.title}
projectSubheader={ getSubheaderText() }
url={"https://tracker.alexav.gg/?type=tv&id=" + data.data.id}
projectImage={data.data.image}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
-38
View File
@@ -1,38 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
export default function Footer() {
// states
const [ mailState, setMailState ] = useState<boolean>(false);
useEffect(() => {
if (mailState) {
setTimeout(() => {
setMailState(false)
}, 3000)
}
}, [mailState])
return (
<div className="lg:w-1/2 md:w-full mx-auto rounded-t-lg text-center p-4 select-none">
<h1 className="font-bold text-sm">&copy; {new Date().getFullYear()}, Alex Frantz. All Rights Reserved</h1>
<p>All logos & images on this page belong to their respective owners.</p>
<hr className="my-2 w-1/2 mx-auto" />
{
mailState ? (
<p className="font-bold hover:underline max-w-fit mx-auto">
Copied!
</p>
) : (
<button onClick={() => {
setMailState(true)
navigator.clipboard.writeText("alex@vierfrantz.com")
}} className="hover:underline">alex@vierfrantz.com</button>
)
}
</div>
)
}
-44
View File
@@ -1,44 +0,0 @@
"use client"
import { useState, useEffect } from 'react';
import Link from "next/link"
export default function Header() {
const quoteArray = [
"It works on my machine",
"Revert 'Revert 'Revert",
"'fuck the tests'",
"this is so cooked",
"Professional Production Tester",
"I break hearts to save lives.",
"go bills",
"josh allen is my goat",
"lets go buffalo",
"oorah",
"how bout them bills??",
"where else would you rather be..",
"..than right here, right now?!"
]
const [ quote, setQuote ] = useState<string>(quoteArray[0])
useEffect(() => {
setQuote(quoteArray[Math.floor(Math.random() * quoteArray.length)]);
}, [])
return (
<div className="bg-zinc-900 p-4 flex justify-between">
<div>
<Link href="/" className="hover:underline">
<h1 className="text-2xl font-bold">Alex Frantz</h1>
</Link>
<p onClick={() => {
setQuote(quoteArray[Math.floor(Math.random() * quoteArray.length)])
}} className="opacity-50 italic select-none">{quote}</p>
</div>
<div className="space-x-2 items-center justify-center flex select-none">
<a className="hover:underline" href="#projects">Projects</a>
<a className="hover:underline" href="#data">Data</a>
<a className="hover:underline" href="#contact">Contact</a>
</div>
</div>
)
}
-107
View File
@@ -1,107 +0,0 @@
export const GitHub = () => {
return (
<svg
role="img"
viewBox="0 0 24 24"
className="w-7 mx-auto bg-white p-1 rounded-lg inline"
xmlns="http://www.w3.org/2000/svg">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205
11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176
0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12
3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015
3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
)
}
export const LastFM = () => {
return (
<svg
role="img"
viewBox="0 0 24 24"
className="w-7 mx-auto bg-white p-1 rounded-lg inline"
xmlns="http://www.w3.org/2000/svg">
<title>Last.fm</title>
<path d="M10.584 17.21l-.88-2.392s-1.43 1.594-3.573 1.594c-1.897 0-3.244-1.649-3.244-4.288 0-3.382 1.704-4.591 3.381-4.591
2.42 0 3.189 1.567 3.849 3.574l.88 2.749c.88 2.666 2.529 4.81 7.285 4.81 3.409 0 5.718-1.044 5.718-3.793
0-2.227-1.265-3.381-3.63-3.931l-1.758-.385c-1.21-.275-1.567-.77-1.567-1.595 0-.934.742-1.484 1.952-1.484 1.32 0 2.034.495 2.144
1.677l2.749-.33c-.22-2.474-1.924-3.492-4.729-3.492-2.474 0-4.893.935-4.893 3.932 0 1.87.907 3.051 3.189 3.601l1.87.44c1.402.33
1.869.907 1.869 1.704 0 1.017-.99 1.43-2.86 1.43-2.776 0-3.93-1.457-4.59-3.464l-.907-2.75c-1.155-3.573-2.997-4.893-6.653-4.893C2.144
5.333 0 7.89 0 12.233c0 4.18 2.144 6.434 5.993 6.434 3.106 0 4.591-1.457 4.591-1.457z"/></svg>
)
}
export const Twitter = () => {
return (
<svg
role="img"
viewBox="0 0 24 24"
className="w-7 mx-auto bg-white p-1 rounded-lg inline"
xmlns="http://www.w3.org/2000/svg">
<title>X</title>
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0
1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/>
</svg>
)
}
export const Medal = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 mx-auto bg-white p-1 rounded-lg inline" x="0px" y="0px" viewBox="0,0,256,256">
<g fill="none" fillRule="nonzero" stroke="none" strokeWidth="1" strokeLinecap="butt" strokeLinejoin="miter" strokeMiterlimit="10"
strokeDasharray="" strokeDashoffset="0" fontFamily="none" fontWeight="none" fontSize="none" textAnchor="none"
>
<g transform="scale(5.33333,5.33333)">
<path d="M40,18.723l-8.239,4.777l8.239,4.777z" fill="none">
</path>
<path d="M8,28.277l8.239,-4.777l-8.239,-4.777z" fill="none">
</path>
<path d="M8,18.723l8.239,4.777l7.761,-4.5l-16,-9l-8,4.085v18.83l8,-4.638z" fill="#000000">
</path>
<path d="M40,10l-16,9l7.761,4.5l8.239,-4.777v9.554l8,4.638v-18.83z" fill="#000000">
</path><path d="M31.761,23.5l-7.761,4.5l16,9v-8.723z" fill="#000000">
</path><path d="M8,28.277v8.723l16,-9l-7.761,-4.5z" fill="#000000">
</path>
<path d="M0,32.915l8,4.085v-8.723z" fill="#060606"></path>
<path d="M24,19l-7.761,4.5l7.761,4.5l7.761,-4.5z" fill="#060606">
</path>
<path d="M40,37l8,-4.085l-8,-4.638z" fill="#060606">
</path>
</g>
</g>
</svg>
)
}
export const Link = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7 mx-auto bg-white p-1 rounded-lg inline" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" stroke="black" strokeWidth="2" d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path fill="none" stroke="black" strokeWidth="2" d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
)
}
export const LinkedIn = () => {
return (
<svg className="w-7 h-7 mx-auto bg-white p-1 rounded-lg inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="#000000"
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
/>
</svg>
)
}
export const Bluesky = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7 mx-auto bg-white p-1 rounded-lg inline" viewBox="0 0 568 501" width="568" height="501">
<path
fill="black"
d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 375.812 284.017 372.431 284 375.306C283.983 372.431 282.831 375.812 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0535 296.954 15.7778 224.501C9.94525 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z" />
</svg>
)
}
-42
View File
@@ -1,42 +0,0 @@
export default function InputBox({
name,
placeholder = "",
isText = true,
disabled = false,
type = "text",
onChange
}: {
name: string,
isText?: boolean,
disabled?: boolean,
placeholder?: string,
type?: string,
onChange?: (value: string) => void
}) {
if (isText) {
return (
<div className="block">
<input
type={type}
name={name}
placeholder={placeholder}
className="rounded-lg p-2 w-1/2 text-black"
onChange={onChange ? ({target}) => onChange(target.value) : undefined}
disabled={disabled}
/>
</div>
)
} else {
return (
<div className="block">
<textarea
name={name}
placeholder={placeholder}
onChange={onChange ? ({ target }) => onChange(target.value) : undefined}
disabled={disabled}
className="rounded-lg p-2 w-1/2 h-32 text-black"
/>
</div>
)
}
}
+63
View File
@@ -0,0 +1,63 @@
import { useState, useEffect } from 'react';
type MusicObject = {
artist: {
mbid: string,
"#text": string
},
name: string,
image: ImageObject[],
url: string
}
type ImageObject = {
size: "small" | "medium" | "large" | "extralarge",
"#text": string
}
export default function MusicComponent() {
const [ music, setMusic ] = useState<MusicObject | null>(null);
const [ failedFetch, setFailedFetch ] = useState(false);
useEffect(() => {
async function fetchMusic() {
const response = await fetch("https://api.alexav.gg/v4/social/music")
if (response.ok) {
const data = await response.json()
setMusic(data.data.tracks[0] as MusicObject)
} else setFailedFetch(true)
}
setTimeout(() => {
fetchMusic()
}, 60000)
fetchMusic()
}, [])
if (music) {
return (
<div className="flex align-middle">
<img width="50" src={music.image[1]['#text']} />
<div className="pl-2">
<h1>{music.name}</h1>
<p>{music.artist['#text']}</p>
</div>
</div>
)
} else {
if (failedFetch) {
return (
<div>
<h1>uh oh!</h1>
<p>failed to get music :(</p>
</div>
)
} else {
return (
<div>
<h1>loading music..</h1>
<p>loading artist..</p>
</div>
)
}
}
}
-30
View File
@@ -1,30 +0,0 @@
export default function ProjectComponent({
urlTo,
description,
name,
tags
}: {
urlTo: string,
description: string,
name: string,
tags: string[]
}) {
return (
<a href={urlTo} target="_blank">
<div className="bg-white text-black mx-auto rounded-lg overflow-hidden">
<div className="p-4 space-y-2">
<h1 className="font-bold text-xl">{name}</h1>
<p>{description}</p>
<div className="space-x-2">
{
tags.map((v, k) => (
<div className="bg-zinc-600 inline max-w-fit p-[5px] text-xs font-bold rounded-xl bg-opacity-20" key={k}>{v}</div>
))
}
</div>
</div>
</div>
</a>
)
}
+60
View File
@@ -0,0 +1,60 @@
import { useState, useEffect } from 'react';
type TVObject = {
id: number,
type: string,
title: string,
author: string | null,
description: string,
image: string,
episode: string,
tmdbId: string,
finished: boolean,
yearReleased: string,
genre: string,
startedOn: string
}
export default function TVComponent() {
const [ show, setShow ] = useState<TVObject | null>(null);
const [ failedFetch, setFailedFetch ] = useState(false);
useEffect(() => {
async function fetchMusic() {
const response = await fetch("https://api.alexav.gg/v4/tracker/latest/tv")
if (response.ok) {
const data = await response.json()
setShow(data.data as TVObject)
} else setFailedFetch(true)
}
setTimeout(() => {
fetchMusic()
}, 60000)
fetchMusic()
}, [])
if (show) {
return (
<div className="flex align-middle">
<a className="hover:underline" href={`https://tracker.alexav.gg/?id=${show.id}&type=${show.type}`} target="_blank">
<h1 className="lowercase">{show.title} - {show.finished ? "finished" : show.episode}</h1>
</a>
</div>
)
} else {
if (failedFetch) {
return (
<div>
<p>failed to get show :(</p>
</div>
)
} else {
return (
<div>
<p>loading show..</p>
</div>
)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import { createRoot } from "react-dom/client";
import "./assets/main.css";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<App />,
);
-72
View File
@@ -1,72 +0,0 @@
type WatchData = {
data: {
id: number,
title: string,
description: string,
author?: string,
image: string,
episode?: string,
tmdbId?: string,
finished: boolean,
startedOn?: Date,
createdAt: Date,
updatedAt: Date,
createdById: string
}
}
type MusicData = {
artist: {
mbid: string,
"#text": string
},
mbid: string,
album: {
mbid: string,
"#text": string,
},
name: string,
url: string,
image: MusicImage[]
}
type MusicImage = {
size: string,
"#text": string
}
type GameData = {
name: string,
location: string,
startedOn: number,
finished: boolean
}
type MedalClip = {
contentId: string,
contentTitle: string,
contentViews: number,
contentLikes: number,
contentThumbnail: string,
directClipUrl: string
}
type ContentTrackerItem = {
id: number,
title: string,
author?: string,
description: string,
episode?: string,
image: string,
type: "tv" | "book" | "game",
tmdbId?: string,
yearReleased: number,
updatedAt: Date,
genre: string,
startedOn: Date,
finished: boolean
}
export type {
WatchData,
MusicData,
GameData,
MedalClip,
ContentTrackerItem
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
-18
View File
@@ -1,18 +0,0 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [],
} satisfies Config;
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+5 -25
View File
@@ -1,27 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
});