new site
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 801 B |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 222 KiB |
@@ -1,4 +0,0 @@
|
||||
# allow all crawlers idc
|
||||
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'm Alex</h1>
|
||||
<p>I'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>
|
||||
);
|
||||
}
|
||||
@@ -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'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'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's dedication to privacy <a className='underline' href="https://discord.com/privacy" target="_blank">here</a>.</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
font-family: 'Pathway Gothic One', sans-serif;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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={"/"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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">© {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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 />,
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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;
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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()],
|
||||
});
|
||||