pip :)
This commit is contained in:
@@ -1,30 +1,29 @@
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import type { MouseEventHandler } from "react"
|
|
||||||
|
|
||||||
type HeaderItemProps = {
|
type HeaderItemProps = {
|
||||||
content: string,
|
content: string,
|
||||||
linkTo: string,
|
linkTo: string,
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>, // don't even wanna bother
|
|
||||||
newWindow?: boolean
|
newWindow?: boolean
|
||||||
}
|
}
|
||||||
export default function HeaderItem(props: HeaderItemProps) {
|
export default function HeaderItem({
|
||||||
if (props.onClick) {
|
content,
|
||||||
return (
|
linkTo,
|
||||||
<>
|
newWindow
|
||||||
<button className="inline hover:text-gray-500 hover:underline pr-2 "
|
}: HeaderItemProps) {
|
||||||
onClick={props.onClick}
|
return (
|
||||||
>
|
<Link
|
||||||
{props.content}
|
className="inline text-green-500 hover:underline font-bold text-3xl pr-2"
|
||||||
</button>
|
href={linkTo}
|
||||||
</>
|
target={newWindow ? "_blank" : "_self"}
|
||||||
)
|
>
|
||||||
} else {
|
{content}
|
||||||
return (
|
</Link>
|
||||||
<p className="inline pr-2 hover:text-gray-500 hover:underline">
|
// <>
|
||||||
<Link href={props.linkTo} target={props.newWindow ? "_blank" : "_self"}>
|
// <button className="inline text-green-500 hover:underline font-bold text-3xl pr-2"
|
||||||
{props.content}
|
// onClick={props.onClick}
|
||||||
</Link>
|
// >
|
||||||
</p>
|
// {props.content}
|
||||||
)
|
// </button>
|
||||||
}
|
// </>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
type InputOptionProps = {
|
|
||||||
label: string,
|
|
||||||
inputName: string,
|
|
||||||
isTextArea?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InputOption(props: InputOptionProps) {
|
|
||||||
return (
|
|
||||||
<div className="pt-2 pb-2">
|
|
||||||
<label className="font-bold">{props.label}</label><br />
|
|
||||||
{
|
|
||||||
props.isTextArea ?
|
|
||||||
<textarea className="bg-gray-800 rounded-sm text-white resize-none h-32 w-72" name={props.inputName} />
|
|
||||||
:
|
|
||||||
<input className="bg-gray-800 rounded-sm text-white" name={props.inputName} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
export default function MusicCard() {
|
|
||||||
const {data: musicData, fetchStatus} = api.misc.song.useQuery()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white text-black max-w-fit mx-auto p-4">
|
|
||||||
<h1 className="text-2xl font-bold inline">Listening To</h1>
|
|
||||||
|
|
||||||
{/* link to lastfm */}
|
|
||||||
<Link
|
|
||||||
href="https://last.fm/user/lulawex"
|
|
||||||
target="_blank"
|
|
||||||
className="opacity-75 underline font-bold text-sm ml-2 hover:text-zinc-600"
|
|
||||||
>
|
|
||||||
Powered by Last.fm
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{
|
|
||||||
fetchStatus !== "fetching" ?
|
|
||||||
<div className="space-y-2">
|
|
||||||
{
|
|
||||||
musicData?.track.map((v,k) => {
|
|
||||||
return <div key={k}>
|
|
||||||
<Image
|
|
||||||
src={v.image[0]?.["#text"] ?? "https://storage.thatalex.dev/content/pfp.jpg"}
|
|
||||||
width="50"
|
|
||||||
height="50"
|
|
||||||
alt={`${v.name} Cover Art`}
|
|
||||||
className="inline mr-2"
|
|
||||||
/>
|
|
||||||
<div className="inline-block align-middle">
|
|
||||||
<Link href={v.url} target="_blank">
|
|
||||||
<h1 className="font-bold w-52 truncate hover:underline">{v.name}</h1>
|
|
||||||
</Link>
|
|
||||||
<p className="opacity-75">{v.artist["#text"]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
<p className="font-bold">Hold tight, fetching..</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export default function NotFound() {
|
|
||||||
return (
|
|
||||||
<div className="text-white text-center">
|
|
||||||
<h1 className="font-bold text-2xl">Well, this is awkward..</h1>
|
|
||||||
<p>There's nothing here right now :/</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
type ProjectCardProps = {
|
|
||||||
name: string,
|
|
||||||
technologies: string[],
|
|
||||||
subHeading?: string,
|
|
||||||
description: string,
|
|
||||||
urlTo: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectCard(props: ProjectCardProps) {
|
|
||||||
const technologies = props.technologies.map((v,k) => {
|
|
||||||
return <div key={k} className="inline">
|
|
||||||
<div className="max-w-fit p-1 inline">
|
|
||||||
{v}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-zinc-700 max-w-lg">
|
|
||||||
<div className="p-2 space-y-2 space-x-2">
|
|
||||||
<h1 className="font-bold text-2xl inline pl-2">{props.name}</h1>
|
|
||||||
<div className="inline-block">
|
|
||||||
{technologies}
|
|
||||||
</div>
|
|
||||||
<hr className="max-w-xs" />
|
|
||||||
<p>{props.description}</p>
|
|
||||||
|
|
||||||
<div className="bg-zinc-900 max-w-fit p-2 rounded-lg">
|
|
||||||
<Link href={props.urlTo} target="_blank">
|
|
||||||
<p>Source</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import Image from "next/image"
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
type ReviewCardProps = {
|
|
||||||
title: string,
|
|
||||||
image: string,
|
|
||||||
url: string,
|
|
||||||
showText: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ReviewCard(props: ReviewCardProps) {
|
|
||||||
return (
|
|
||||||
<Link href={props.url}>
|
|
||||||
<div className="inline">
|
|
||||||
<Image alt={props.title + " cover"} width="160" height="160" src={props.image} className="p-2" />
|
|
||||||
{
|
|
||||||
props.showText ?
|
|
||||||
<p className="font-bold pt-2 text-center">{props.title}</p>
|
|
||||||
:
|
|
||||||
""
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,51 +1,21 @@
|
|||||||
// component imports
|
// component imports
|
||||||
import Header from "./header";
|
|
||||||
import Footer from "./footer";
|
|
||||||
import Head from "./Head"
|
import Head from "./Head"
|
||||||
|
|
||||||
// react imports
|
// react imports
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// next modules
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import Router from 'next/router';
|
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
protectedPage?: boolean
|
protectedPage?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({ children, protectedPage }: LayoutProps) {
|
export default function RootLayout({ children }: LayoutProps) {
|
||||||
const {status} = useSession()
|
|
||||||
|
|
||||||
if (protectedPage) {
|
|
||||||
if (status === "loading") {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen bg-black">
|
|
||||||
<div className="w-1/2 mx-auto">
|
|
||||||
<Head />
|
|
||||||
<p>Loading...</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
} else if (status === "unauthenticated") {
|
|
||||||
Router.replace("/").catch(() => {
|
|
||||||
console.error("Failed to reroute user!")
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<></>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black">
|
<main className="bg-black h-screen flex flex-col justify-center items-center overflow-y-hidden">
|
||||||
<div className="lg:w-1/2 sm:w-full mx-auto">
|
<Head />
|
||||||
<Head />
|
<div className="flicker">
|
||||||
<Header />
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,103 @@
|
|||||||
import Link from "next/link"
|
import Image from "next/image";
|
||||||
import { Trash } from "grommet-icons"
|
import Link from "next/link";
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import type {
|
||||||
|
MedalApiObject,
|
||||||
|
SteamApiObject,
|
||||||
|
MusicApiObject
|
||||||
|
} from '@/utils/types'
|
||||||
|
|
||||||
type SocialCardProps = {
|
type SocialCardProps = {
|
||||||
name: string,
|
platform: "steam" | "music" | "medal"
|
||||||
id: number,
|
|
||||||
url: string,
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
deleteCallback: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SocialMediaCard(props: SocialCardProps) {
|
function MedalSocialCard() {
|
||||||
return (
|
const { data: medalData, medalFetchStatus } = api.social.getMedalData.useQuery();
|
||||||
<div className="bg-white p-5 w-full text-left">
|
|
||||||
<Link href={props.url} target="_blank">
|
|
||||||
<h1 className="inline font-bold">{props.name}</h1>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{ props.isAuthenticated && (
|
return (
|
||||||
<>
|
<>
|
||||||
<Trash onClick={props.deleteCallback} color="black" className="float-right inline hover:cursor-pointer" />
|
{
|
||||||
<p className="float-right opacity-50 font-bold inline mr-2">ID: {props.id}</p>
|
medalData?.data.map((v: MedalApiObject,k: number) => {
|
||||||
</>
|
return <div key={k}>
|
||||||
)}
|
<Image
|
||||||
</div>
|
src={v.image[0]?.["#text"] ?? "https://storage.thatalex.dev/content/pfp.jpg"}
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
alt={`${v.name} Cover Art`}
|
||||||
|
className="inline mr-2"
|
||||||
|
/>
|
||||||
|
<div className="inline-block align-middle">
|
||||||
|
<Link href={v.url} target="_blank">
|
||||||
|
<h1 className="font-bold w-52 truncate hover:underline">{v.name}</h1>
|
||||||
|
</Link>
|
||||||
|
<p className="opacity-75">{v.artist["#text"]}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SteamSocialCard() {
|
||||||
|
const { data: SteamData, steamFetchStatus } = api.social.getSteamData.useQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function MusicSocialCard() {
|
||||||
|
const { data: MusicData, musicFetchStatus } = api.social.getMusicData.useQuery();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SocialCard({ platform }: SocialCardProps) {
|
||||||
|
const [content, setContent] = useState<React.JSX.Element | null>(null)
|
||||||
|
|
||||||
|
|
||||||
|
const platformDisplayElements = () => {
|
||||||
|
if (platform === "music") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{
|
||||||
|
socialData?.data.map((v,k) => {
|
||||||
|
return <div key={k}>
|
||||||
|
<Image
|
||||||
|
src={v.image[0]?.["#text"] ?? "https://storage.thatalex.dev/content/pfp.jpg"}
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
alt={`${v.name} Cover Art`}
|
||||||
|
className="inline mr-2"
|
||||||
|
/>
|
||||||
|
<div className="inline-block align-middle">
|
||||||
|
<Link href={v.url} target="_blank">
|
||||||
|
<h1 className="font-bold w-52 truncate hover:underline">{v.name}</h1>
|
||||||
|
</Link>
|
||||||
|
<p className="opacity-75">{v.artist["#text"]}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="bg-white text-black max-w-fit mx-auto p-4">
|
||||||
|
// <h1 className="text-2xl font-bold inline">{displayPlatform}</h1>
|
||||||
|
// </div>
|
||||||
|
// // <div className="bg-white text-black max-w-fit mx-auto p-4">
|
||||||
|
// // <h1 className="text-2xl font-bold inline">Listening To</h1>
|
||||||
|
|
||||||
|
// // {/* link to lastfm */}
|
||||||
|
// // <Link
|
||||||
|
// // href="https://last.fm/user/lulawex"
|
||||||
|
// // target="_blank"
|
||||||
|
// // className="opacity-75 underline font-bold text-sm ml-2 hover:text-zinc-600"
|
||||||
|
// // >
|
||||||
|
// // Powered by Last.fm
|
||||||
|
// // </Link>
|
||||||
|
// // </div>
|
||||||
|
// )
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@ export default function Footer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative sm:w-1/2 m-auto mt-5 rounded-t-sm bg-zinc-900 p-2 text-white">
|
<div className="relative rounded-t-sm p-2 text-white opacity-50">
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
<Link href="mailto:alex@alexavfrantz.com" className="hover:text-gray-500 hover:underline hover:cursor-pointer">
|
<Link href="mailto:alex@alexavfrantz.com" className="hover:text-gray-500 hover:underline hover:cursor-pointer">
|
||||||
alex@alexavfrantz.com
|
alex@alexavfrantz.com
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-center">
|
||||||
|
The 'PipBoy' is owned by Bethesda, I'm just doing this for fun!
|
||||||
|
</p>
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
© Alex Frantz, {date.getFullYear()}
|
© Alex Frantz, {date.getFullYear()}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+15
-21
@@ -1,31 +1,25 @@
|
|||||||
import {useSession, signIn, signOut} from 'next-auth/react';
|
|
||||||
import HeaderItem from "./HeaderItem";
|
import HeaderItem from "./HeaderItem";
|
||||||
|
|
||||||
export default function Header() {
|
type HeaderProps = {
|
||||||
const { status } = useSession()
|
isNotHomePage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header({
|
||||||
|
isNotHomePage = false
|
||||||
|
}: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-white max-w-sm mx-auto pt-2 pb-4">
|
<div className="text-white max-w-sm mx-auto pt-2 pb-4">
|
||||||
<div className="inline-block pb-2">
|
<div className="inline-block pb-2 space-x-6 text-center">
|
||||||
<HeaderItem content="home" linkTo="/" />
|
<HeaderItem content="HOME" linkTo={
|
||||||
<HeaderItem content="links" linkTo="/links" />
|
isNotHomePage ? "/?noAnimation"
|
||||||
<HeaderItem content="projects" linkTo="/projects" />
|
: "/"
|
||||||
{ status === "authenticated" &&
|
} />
|
||||||
<HeaderItem
|
<HeaderItem content="LINKS" linkTo="/links" />
|
||||||
content="create"
|
<HeaderItem content="PROJECTS" linkTo="/projects" />
|
||||||
linkTo="/create"
|
<HeaderItem content="ABOUT" linkTo="/about" />
|
||||||
/>
|
|
||||||
}
|
|
||||||
<HeaderItem content={status == "unauthenticated" ? "login" : "logout" } onClick={async () => {
|
|
||||||
if (status == "unauthenticated") {
|
|
||||||
await signIn()
|
|
||||||
} else {
|
|
||||||
await signOut()
|
|
||||||
}
|
|
||||||
}} linkTo="" />
|
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr className="border-green-500" />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import Head from '@/components/Head'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
|
||||||
return (
|
|
||||||
<main className="bg-black flex min-h-screen">
|
|
||||||
<Head />
|
|
||||||
<div className="m-auto text-white text-center">
|
|
||||||
<h1 className="text-center text-2xl font-bold">Well, this is awkward..</h1>
|
|
||||||
<p>There is nothing here. An endless void, if you will.</p>
|
|
||||||
<p>How about we <span className="underline hover:text-gray-500"><Link href="/">head back?</Link></span> Or you could check out <span className="underline hover:text-gray-500"><Link target="_blank" href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">this cool video I found.</Link></span></p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ export default function Document() {
|
|||||||
<Head>
|
<Head>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import RootLayout from "@/components/RootLayout";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Header from "@/components/header";
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<RootLayout>
|
||||||
|
<Header isNotHomePage={true} />
|
||||||
|
<div className="text-center text-green-500">
|
||||||
|
<h1 className="font-bold text-2xl">About this website</h1>
|
||||||
|
<p className="max-w-10">This is just a hobby project I thought was cool!<br /> I plan to play with it a bit to grasp more CSS, but so far, pretty cool right?</p>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// react modules
|
|
||||||
import {useState} from 'react'
|
|
||||||
import type {FormEvent} from 'react';
|
|
||||||
|
|
||||||
// trpc api
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import RootLayout from '@/components/RootLayout';
|
|
||||||
import InputOption from '@/components/InputOption';
|
|
||||||
|
|
||||||
export default function CreatePage() {
|
|
||||||
// ui states
|
|
||||||
const [message, setMessage] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// trpc hooks
|
|
||||||
const linksMutation = api.links.create.useMutation()
|
|
||||||
|
|
||||||
function onLinkCreateSubmit(event: FormEvent<HTMLFormElement>) {
|
|
||||||
const form = event.target as HTMLFormElement
|
|
||||||
const urlRegex = new RegExp("^((https):\/\/)?(www.)?[a-z0-9]+\.[a-z]+(\/[a-zA-Z0-9#]+\/?)*$")
|
|
||||||
|
|
||||||
const titleElement = form.elements.namedItem('title') as HTMLInputElement;
|
|
||||||
const urlElement = form.elements.namedItem('url') as HTMLInputElement;
|
|
||||||
const orderElement = form.elements.namedItem("orderNumber") as HTMLInputElement;
|
|
||||||
const urlMatches = urlElement.value.match(urlRegex)
|
|
||||||
|
|
||||||
if (!titleElement.value || !urlElement.value || (orderElement.value && isNaN(Number(orderElement.value)))) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
setMessage("One or more values were missing!")
|
|
||||||
setTimeout(() => setMessage(null), 5000)
|
|
||||||
} else if (urlMatches != null && !urlMatches[1]) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
setMessage("An invalid URL was provided!")
|
|
||||||
setTimeout(() => setMessage(null), 5000)
|
|
||||||
} else {
|
|
||||||
const object = {
|
|
||||||
name: titleElement.value,
|
|
||||||
url: urlElement.value,
|
|
||||||
orderNumber: Number(orderElement.value) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
linksMutation.mutate(object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onLinkUpdateSubmit(event: FormEvent<HTMLFormElement>) {
|
|
||||||
const form = event.target as HTMLFormElement
|
|
||||||
const urlRegex = new RegExp("^((https):\/\/)?(www.)?[a-z0-9]+\.[a-z]+(\/[a-zA-Z0-9#]+\/?)*$")
|
|
||||||
|
|
||||||
const idElement = form.elements.namedItem("id") as HTMLInputElement;
|
|
||||||
const titleElement = form.elements.namedItem('title') as HTMLInputElement;
|
|
||||||
const urlElement = form.elements.namedItem('url') as HTMLInputElement;
|
|
||||||
const orderElement = form.elements.namedItem("orderNumber") as HTMLInputElement;
|
|
||||||
const urlMatches = urlElement.value.match(urlRegex)
|
|
||||||
|
|
||||||
if (!idElement.value || isNaN(Number(idElement.value)) || (orderElement.value && isNaN(Number(orderElement.value)))) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
setMessage("One or more values were missing!")
|
|
||||||
setTimeout(() => setMessage(null), 5000)
|
|
||||||
} else if (urlMatches != null && !urlMatches[1]) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
setMessage("An invalid URL was provided!")
|
|
||||||
setTimeout(() => setMessage(null), 5000)
|
|
||||||
} else {
|
|
||||||
const object = {
|
|
||||||
id: Number(idElement.value),
|
|
||||||
name: titleElement.value,
|
|
||||||
url: urlElement.value,
|
|
||||||
orderNumber: Number(orderElement.value) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
linksMutation.mutate(object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RootLayout protectedPage={true}>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="w-96 p-2 mx-auto bg-gray-500 text-center text-white">
|
|
||||||
<h1 className="font-bold text-2xl">Create Link</h1>
|
|
||||||
|
|
||||||
<div className="pt-2 pb-2">
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={onLinkCreateSubmit}>
|
|
||||||
<InputOption inputName="title" label="Title" />
|
|
||||||
<InputOption inputName="orderNumber" label="Order Number" />
|
|
||||||
<InputOption inputName="url" label="URL" />
|
|
||||||
|
|
||||||
<button className="bg-gray-800 text-white rounded-lg p-2" type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>{message}</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-96 p-2 mx-auto bg-gray-500 text-center text-white">
|
|
||||||
<h1 className="font-bold text-2xl">Update Link</h1>
|
|
||||||
|
|
||||||
<div className="pt-2 pb-2">
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={onLinkUpdateSubmit}>
|
|
||||||
<InputOption inputName="id" label="Link ID" />
|
|
||||||
<InputOption inputName="title" label="Title" />
|
|
||||||
<InputOption inputName="orderNumber" label="Order Number" />
|
|
||||||
<InputOption inputName="url" label="URL" />
|
|
||||||
|
|
||||||
<button className="bg-gray-800 text-white rounded-lg p-2" type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>{message}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RootLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+83
-53
@@ -1,61 +1,91 @@
|
|||||||
import RootLayout from "@/components/RootLayout";
|
import RootLayout from '@/components/RootLayout';
|
||||||
import MusicCard from "@/components/MusicCard";
|
import Header from '@/components/header';
|
||||||
import Link from 'next/link';
|
import Footer from '@/components/footer';
|
||||||
import { Cli } from "grommet-icons";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [doAnimation, setDoAnimation] = useState<boolean>(false);
|
||||||
|
const [pastLogo, setPastLogo] = useState<boolean>(false);
|
||||||
|
const [text, setText] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const generateRandomTextString = () => {
|
||||||
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
const length = 66;
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * characters.length)) + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.href.includes("?noAnimation")) {
|
||||||
|
const logoElement = document.getElementById("pipboy");
|
||||||
|
const textElement = document.getElementById("textwall");
|
||||||
|
const mainElement = document.getElementById("mainelement");
|
||||||
|
|
||||||
|
if (logoElement && textElement && mainElement) {
|
||||||
|
logoElement.style.display = "none";
|
||||||
|
textElement.style.display = "none";
|
||||||
|
mainElement.style.display = "block";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setDoAnimation(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (doAnimation) {
|
||||||
|
const element = document.getElementById("pipboy")
|
||||||
|
if (element) {
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.display = "none";
|
||||||
|
setPastLogo(true)
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [doAnimation])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textElement = document.getElementById("textwall");
|
||||||
|
const mainElement = document.getElementById("mainelement");
|
||||||
|
if (pastLogo && doAnimation) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setText((prevState) => {
|
||||||
|
return [...prevState, generateRandomTextString()]
|
||||||
|
})
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(interval)
|
||||||
|
if (textElement && mainElement) {
|
||||||
|
textElement.style.display = "none"
|
||||||
|
mainElement.style.display = "block"
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}, [pastLogo, doAnimation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootLayout>
|
<RootLayout>
|
||||||
<div className="text-white">
|
{/* "logo", hidden after 4 seconds */}
|
||||||
<div className="text-center">
|
<h1 className="text-green-500 text-9xl font-bold italic" id="pipboy">Pip-Boy</h1>
|
||||||
<h1 className="text-4xl font-bold">Alex Frantz</h1>
|
|
||||||
<p>QA Engineer, Zach Bryan Enthusiast</p>
|
{/* text wall, hidden until pastLogo */}
|
||||||
</div>
|
<p className="text-green-500 text-2xl max-w-10 flex-shrink-0" id="textwall">{text}</p>
|
||||||
|
|
||||||
<hr className="h-px my-2 bg-gray-200 border-0 w-1/2 mx-auto dark:bg-gray-700" />
|
{/* and the content :) */}
|
||||||
|
<div className="text-green-500 flicker max-w-1/2 hidden" id="mainelement">
|
||||||
<div className="my-4">
|
<Header isNotHomePage={false} />
|
||||||
<div className="bg-terminalback lg:max-w-xl md:max-w-xl sm:max-w-full m-auto">
|
<div className="text-left">
|
||||||
<div className="bg-terminalbar">
|
<p className="text-md">
|
||||||
<div className="pt-2 pl-2">
|
<span className="font-bold text-2xl">Hello traveller!</span>
|
||||||
<div className="bg-terminalback w-60 rounded-t-md">
|
</p>
|
||||||
<div className="p-2 pl-4 inline-block">
|
<p>Welcome to my website! Feel free to explore & learn more about me :)</p>
|
||||||
<Cli className="pr-2" />
|
|
||||||
<p className="inline">
|
|
||||||
Command Prompt
|
|
||||||
</p>
|
|
||||||
<p className="float-right ml-6">
|
|
||||||
<Link target="_blank" href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
|
|
||||||
X
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* content of cmd */}
|
|
||||||
<div className="p-2 font-mono">
|
|
||||||
<p>Michaelsoft Binbows [Version 10.0.23420.6969]</p>
|
|
||||||
<p>(c) Pixelpeek Development. No Rights Reserved.</p>
|
|
||||||
<br />
|
|
||||||
<p>C:\Users\CoolGuy{'>'}whoami</p>
|
|
||||||
<p>Full Name: <span className="font-bold">Alex Frantz</span></p>
|
|
||||||
<p>Based in: <span className="font-bold">Western New York</span></p>
|
|
||||||
<p>Working at: <span className="font-bold hover:text-medal underline"><Link target="_blank" href="https://medal.tv">Medal.tv</Link></span> as a <span className="font-bold">QA Engineer</span></p>
|
|
||||||
<p>Skills: <span className="font-bold">Java(Type)Script</span>, <span className="font-bold">Python</span>, <span className="font-bold">Go</span></p>
|
|
||||||
<p>Learning: <span className="font-bold">Network Engineering</span>, <span className="font-bold">Game Dev</span>, <span className="font-bold">Frontend</span>, <span className="font-bold">Backend</span></p>
|
|
||||||
<p>Website: <span className="font-bold underline hover:text-gray-500"><Link target="_blank" href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">alexavfrantz.com</Link></span></p>
|
|
||||||
<br />
|
|
||||||
<p>C:\Users\CoolGuy{'>'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className="h-px my-2 bg-gray-200 border-0 w-1/2 mx-auto dark:bg-gray-700" />
|
|
||||||
|
|
||||||
<div className="my-4">
|
|
||||||
<MusicCard />
|
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
+31
-54
@@ -1,61 +1,38 @@
|
|||||||
import SocialCard from '@/components/SocialCard'
|
import RootLayout from "@/components/RootLayout";
|
||||||
import Head from '@/components/Head';
|
import Footer from "@/components/footer";
|
||||||
import { api } from "@/utils/api";
|
import Header from "@/components/header";
|
||||||
import { useSession } from 'next-auth/react';
|
import Link from "next/link";
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export default function Links() {
|
|
||||||
const {status} = useSession();
|
|
||||||
|
|
||||||
// trpc hooks
|
|
||||||
const { data: linkData } = api.links.getAll.useQuery()
|
|
||||||
const deleteMutation = api.links.delete.useMutation()
|
|
||||||
|
|
||||||
|
export default function LinksPage() {
|
||||||
return (
|
return (
|
||||||
<main className="bg-black flex flex-col justify-center items-center min-h-screen">
|
<RootLayout>
|
||||||
<Head />
|
<Header isNotHomePage={true} />
|
||||||
<Link href="/">
|
<div className="text-green-500 text-center">
|
||||||
<h1 className="text-white text-sm underline hover:text-gray-500 pb-2"><- Back Home</h1>
|
<h1 className="font-bold text-3xl">My Links!</h1>
|
||||||
</Link>
|
|
||||||
<div className="bg-zinc-900 p-10 rounded-sm">
|
|
||||||
<div className="pb-2 flex flex-col justify-center items-center">
|
|
||||||
<Image className="rounded-lg" width="90" height="90" alt="Profile Picture" src="https://storage.thatalex.dev/content/pfp.png" />
|
|
||||||
<div className="text-white text-center pt-2">
|
|
||||||
<h1>Alex</h1>
|
|
||||||
<p>I'm Alex, a QA Engineer by day and developer by night</p>
|
|
||||||
<hr className="h-px my-2 bg-gray-200 border-0 dark:bg-gray-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{
|
<Link href="https://twitter.com/avvex__" target="_blank">
|
||||||
linkData && linkData.length > 0 ?
|
<div className="hover:bg-green-500 hover:text-black">
|
||||||
linkData.sort((a,b) => {
|
<h1 className="text-2xl mx-32">Twitter</h1>
|
||||||
if (a.orderNumber < b.orderNumber) {
|
</div>
|
||||||
return -1
|
</Link>
|
||||||
} else if (a.orderNumber > b.orderNumber) {
|
<Link href="https://mast.thatalex.dev/@alex" target="_blank">
|
||||||
return 1
|
<div className="hover:bg-green-500 hover:text-black">
|
||||||
}
|
<h1 className="text-2xl mx-32">Mastodon</h1>
|
||||||
|
</div>
|
||||||
return 0
|
</Link>
|
||||||
}).map((v,k) => {
|
<Link href="https://linkedin.com/in/thatalex" target="_blank">
|
||||||
return <SocialCard
|
<div className="hover:bg-green-500 hover:text-black">
|
||||||
name={v.name}
|
<h1 className="text-2xl mx-32">LinkedIn</h1>
|
||||||
key={k}
|
</div>
|
||||||
id={v.id}
|
</Link>
|
||||||
url={v.url}
|
<Link href="https://medal.tv/users/215577" target="_blank">
|
||||||
deleteCallback={() => {
|
<div className="hover:bg-green-500 hover:text-black">
|
||||||
deleteMutation.mutate(v.id);
|
<h1 className="text-2xl mx-32">Medal.tv</h1>
|
||||||
}}
|
</div>
|
||||||
isAuthenticated={ status === "authenticated" }
|
</Link>
|
||||||
/>
|
|
||||||
})
|
|
||||||
:
|
|
||||||
<h1 className="text-center text-white font-bold">Uh oh, there are no links here!</h1>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<Footer />
|
||||||
|
</RootLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+33
-63
@@ -1,69 +1,39 @@
|
|||||||
import ProjectCard from "@/components/ProjectsCard"
|
import RootLayout from "@/components/RootLayout";
|
||||||
import RootLayout from "@/components/RootLayout"
|
import Footer from "@/components/footer";
|
||||||
|
import Header from "@/components/header";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Projects() {
|
export default function ProjectsPage() {
|
||||||
return (
|
return (
|
||||||
<RootLayout>
|
<RootLayout>
|
||||||
<div className="text-white">
|
<Header isNotHomePage={true} />
|
||||||
<div className="mb-4">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="font-bold text-2xl text-center">Projects</h1>
|
<Link href="https://github.com/azpha/splitstat" target="_blank">
|
||||||
</div>
|
<div className="hover:bg-green-500 hover:text-black text-green-500">
|
||||||
|
<h1 className=" text-2xl mx-32">SplitStat</h1>
|
||||||
<div className="space-y-2 w-full flex flex-col justify-center items-center">
|
</div>
|
||||||
<ProjectCard
|
</Link>
|
||||||
name="SplitStat"
|
<Link href="https://github.com/azpha/medaltv-wrapper" target="_blank">
|
||||||
description="SplitStat was a Discord bot that allowed you to see your stats & other player stats from the Splitgate game. It would also allow you to see the results of an in-game match or what matches a player had played recently."
|
<div className="hover:bg-green-500 hover:text-black text-green-500">
|
||||||
urlTo="https://github.com/azpha/splitstat"
|
<h1 className=" text-2xl mx-32">Medal.tv Wrapper</h1>
|
||||||
technologies={
|
</div>
|
||||||
[
|
</Link>
|
||||||
"JavaScript",
|
<Link href="https://github.com/azpha/mcsrvstat-js" target="_blank">
|
||||||
"Linux"
|
<div className="hover:bg-green-500 hover:text-black text-green-500">
|
||||||
]
|
<h1 className=" text-2xl mx-32">MCSRVSTAT Wrapper</h1>
|
||||||
}
|
</div>
|
||||||
/>
|
</Link>
|
||||||
<ProjectCard
|
<Link href="https://github.com/azpha/pmanager" target="_blank">
|
||||||
name="medaltv-wrapper"
|
<div className="hover:bg-green-500 hover:text-black text-green-500">
|
||||||
description="medaltv-wrapper is a Node.js wrapper for the Medal.tv Public API, allowing you to fetch clips from the platform and use the data in your code."
|
<h1 className=" text-2xl mx-32">PManager</h1>
|
||||||
urlTo="https://github.com/azpha/medaltv-wrapper"
|
</div>
|
||||||
technologies={
|
</Link>
|
||||||
[
|
<Link href="https://github.com/azpha/tv-watchlist" target="_blank">
|
||||||
"JavaScript"
|
<div className="hover:bg-green-500 hover:text-black text-green-500">
|
||||||
]
|
<h1 className=" text-2xl mx-32">Watchlist</h1>
|
||||||
}
|
</div>
|
||||||
/>
|
</Link>
|
||||||
<ProjectCard
|
<Footer />
|
||||||
name="mcsrvstat-wrapper"
|
|
||||||
description="mcsrvstat-wrapper is a Node.js wrapper for MCSRVSTAT API, allowing you to fetch the status of Minecraft Java & Bedrock servers by running 1 method."
|
|
||||||
urlTo="https://github.com/azpha/mcsrvstat-js"
|
|
||||||
technologies={
|
|
||||||
[
|
|
||||||
"JavaScript"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
name="PManager"
|
|
||||||
description="PManager is a simple Express server that allows you to restart/stop/start your PM2 scripts easily remotely for uses like GitHub Actions."
|
|
||||||
urlTo="https://github.com/azpha/pmanager"
|
|
||||||
technologies={
|
|
||||||
[
|
|
||||||
"JavaScript"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
name="Watchlist"
|
|
||||||
description="Watchlist is a NextJS website built on the T3 Stack simply tracking my watch history, displaying stuff like my recently watched TV and Movies."
|
|
||||||
urlTo="https://github.com/azpha/tv-watchlist"
|
|
||||||
technologies={
|
|
||||||
[
|
|
||||||
"Typescript",
|
|
||||||
"NextJS",
|
|
||||||
"tRPC"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { postRouter } from "@/server/api/routers/post";
|
import { postRouter } from "@/server/api/routers/post";
|
||||||
import { linksRouter } from "./routers/links";
|
import { linksRouter } from "./routers/links";
|
||||||
import { miscRouter } from "./routers/misc";
|
import { socialRouter } from "./routers/social";
|
||||||
import { blogRouter } from "./routers/blog";
|
import { blogRouter } from "./routers/blog";
|
||||||
import { createTRPCRouter } from "@/server/api/trpc";
|
import { createTRPCRouter } from "@/server/api/trpc";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import { createTRPCRouter } from "@/server/api/trpc";
|
|||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
post: postRouter,
|
post: postRouter,
|
||||||
links: linksRouter,
|
links: linksRouter,
|
||||||
misc: miscRouter,
|
social: socialRouter,
|
||||||
blog: blogRouter
|
blog: blogRouter
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure
|
|
||||||
} from '@/server/api/trpc';
|
|
||||||
|
|
||||||
type ImageListType = {
|
|
||||||
"size": string,
|
|
||||||
"#text": string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackResponseType = {
|
|
||||||
status: number,
|
|
||||||
track: LastTrackType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type LastTrackType = {
|
|
||||||
"@attr": {
|
|
||||||
"nowplaying": string
|
|
||||||
},
|
|
||||||
"album": {
|
|
||||||
"mbid": string,
|
|
||||||
"#text": string
|
|
||||||
},
|
|
||||||
"artist": {
|
|
||||||
"mbid": string,
|
|
||||||
"#text": string,
|
|
||||||
},
|
|
||||||
"image": ImageListType[],
|
|
||||||
"mbid": string,
|
|
||||||
"name": string,
|
|
||||||
"streamable": number,
|
|
||||||
"url": string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const miscRouter = createTRPCRouter({
|
|
||||||
song: publicProcedure
|
|
||||||
.query(async () => {
|
|
||||||
const response = await axios.get<TrackResponseType>("https://api.thatalex.dev/v3/music/last-track")
|
|
||||||
return response.data
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {z} from 'zod';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
publicProcedure
|
||||||
|
} from '@/server/api/trpc';
|
||||||
|
import type {
|
||||||
|
MedalApiObject,
|
||||||
|
MusicApiObject,
|
||||||
|
SteamApiObject
|
||||||
|
} from '@/utils/types'
|
||||||
|
|
||||||
|
const base_url = "https://api.thatalex.dev/v4/social/"
|
||||||
|
export const socialRouter = createTRPCRouter({
|
||||||
|
getMedalData: publicProcedure
|
||||||
|
.query(async ({input}) => {
|
||||||
|
const response = await axios.get(base_url + "medal")
|
||||||
|
return response.data as MedalApiObject
|
||||||
|
}),
|
||||||
|
getSteamData: publicProcedure
|
||||||
|
.query(async ({input}) => {
|
||||||
|
const response = await axios.get(base_url + "steam")
|
||||||
|
return response.data as SteamApiObject
|
||||||
|
}),
|
||||||
|
getMusicData: publicProcedure
|
||||||
|
.query(async ({input}) => {
|
||||||
|
const response = await axios.get(base_url + "music")
|
||||||
|
return response.data as MusicApiObject
|
||||||
|
})
|
||||||
|
})
|
||||||
+19
-1
@@ -3,5 +3,23 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: "VT323", monospace;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flicker {
|
||||||
|
animation: flicker 0.5s alternate;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user