feat:gallery

This commit is contained in:
Damien Ostler 2024-05-26 02:14:59 -04:00
parent 91ea64d20f
commit 1b5e1da64d
1755 changed files with 902 additions and 138 deletions

View File

@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const galleryId = params.id;
const supabase = createClient();
// List all files in the galleryId path
let { data: files, error } = await supabase.storage.from('galleries').list(galleryId);
if (files==null || error) {
console.error('Error listing files:', error);
return NextResponse.error();
}
const urls = [];
// Loop through each file, download it, convert it to base64, and add the data URL to the array
for (const file of files) {
let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+"/"+file.name);
if (error || blobdata==null) {
console.error('Error downloading file:', error);
continue;
}
const base64 = Buffer.from(await blobdata.arrayBuffer()).toString('base64');
const contentType = file.name.endsWith('.png') ? 'image/png' : 'image/jpeg';
const dataUrl = `data:${contentType};base64,${base64}`;
urls.push(dataUrl);
}
// Return a JSON response with the array of URLs
return new Response(JSON.stringify(urls), { headers: { 'content-type': 'application/json' } });
}

View File

@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const galleryId= params.id // 312
const supabase = createClient();
// Extract galleryId from the route value
var blob = null;
var contentType = "image/jpeg"
let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+'/1.jpeg')
blob = blobdata;
console.log(error)
if (error) {
contentType = "image/png"
let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+'/1.png')
console.log(error)
blob = blobdata;
}
if(blob != null){
const base64 = Buffer.from(await blob.arrayBuffer()).toString('base64');
const dataUrl = `data:${contentType};base64,${base64}`;
return new Response(dataUrl);
}
return NextResponse.error();
}

View File

@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) {
const supabase = createClient();
let { data: galleries, error } = await supabase
.from('galleries')
.select('*')
return NextResponse.json(galleries)
}

View File

@ -1,41 +0,0 @@
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export default async function Gallery() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
return (
<div className="flex-1 w-full flex flex-col gap-20 items-center animate-in">
<div className="absolute w-full h-full">
<img src="gallery_girl.png" className="float-right object-cover h-screen w-3/6 animate-fade-up animate-duration-[3000ms]" alt="Background" />
</div>
<h1 className="animate-jump animate-duration-1000 animate-ease-linear z-10 pt-20 text-7xl text-center text-white text-shadow-purple-grey-glow absolute">Neroshi's Gallery</h1>
<div className="grid grid-cols-3 gap-4 z-50 pt-60 ">
<a href="#" className="animate-fade animate-duration-1000 animate-delay-[2000ms] animate-ease-linear block w-64 h-64 max-w-sm p-6 bg-opacity-50 backdrop-blur rounded-lg shadow hover:bg-gray-100 hover:bg-opacity-15">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white text-shadow-purple-grey-glow">Gallery Name</h5>
</a>
<a href="#" className="animate-fade animate-duration-1000 animate-delay-[2000ms] animate-ease-linear block w-64 h-64 max-w-sm p-6 bg-opacity-50 backdrop-blur rounded-lg shadow hover:bg-gray-100 hover:bg-opacity-15">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white text-shadow-purple-grey-glow">Gallery Name</h5>
</a>
<a href="#" className="animate-fade animate-duration-1000 animate-delay-[2000ms] animate-ease-linear block w-64 h-64 max-w-sm p-6 bg-opacity-50 backdrop-blur rounded-lg shadow hover:bg-gray-100 hover:bg-opacity-15">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white text-shadow-purple-grey-glow">Gallery Name</h5>
</a>
<a href="#" className="animate-fade animate-duration-1000 animate-delay-[2000ms] animate-ease-linear block w-64 h-64 max-w-sm p-6 bg-opacity-50 backdrop-blur rounded-lg shadow hover:bg-gray-100 hover:bg-opacity-15">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white text-shadow-purple-grey-glow">Gallery Name</h5>
</a>
<a href="#" className="animate-fade animate-duration-1000 animate-delay-[2000ms] animate-ease-linear block w-64 h-64 max-w-sm p-6 bg-opacity-50 backdrop-blur rounded-lg shadow hover:bg-gray-100 hover:bg-opacity-15">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white text-shadow-purple-grey-glow">Gallery Name</h5>
</a>
<a href="#" className="animate-fade animate-duration-1000 animate-delay-[2000ms] animate-ease-linear block w-64 h-64 max-w-sm p-6 bg-opacity-50 backdrop-blur rounded-lg shadow hover:bg-gray-100 hover:bg-opacity-15">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white text-shadow-purple-grey-glow">Gallery Name</h5>
</a>
</div>
</div>
);
}

74
app/gallery/page.tsx Normal file
View File

@ -0,0 +1,74 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import { redirect } from "next/navigation";
import { Vortex } from "@/components/ui/vortex";
import GalleryThumbnail from "@/components/ui/gallery_thumbnail";
import React, { useState, useEffect } from 'react';
import { User } from "@supabase/supabase-js";
import Gallery from "@/components/ui/gallery";
function PageComponent() {
const supabase = createClient();
const [isOpen, setIsOpen] = useState<boolean>(false);
const [galleries, setGalleries] = useState<any[]>([]); // replace any with your gallery type
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [selectedGallery, setSelectedGallery] = useState<number | null>(null);
const selectGallery = (gallery:number) => {
setSelectedGallery(gallery);
setIsOpen(true);
};
const getData = async () => {
const galleriesResponse = await fetch('/api/galleries');
const galleriesData = await galleriesResponse.json();
let { data: { user } } = await supabase.auth.getUser();
setGalleries(galleriesData);
setUser(user);
setLoading(false);
}
useEffect(() => {
getData();
}, [selectedGallery,isOpen]);
return ( ( user ? (
<div className="w-full h-full flex justify-center">
<div className="flex-1 w-full h-full flex flex-col gap-20">
<div className="absolute w-full h-full overflow-hidden z-2 animate-jump-in animate-ease-out">
<img src="gallery_girl.png" className="float-right object-cover h-screen w-3/6" alt="Background" />
</div>
<div className="absolute items-center w-3/5 h-full ml-10 z-2 overflow-hidden">
<div className="grid grid-cols-3 gap-x-10 h-full overflow-y-auto no-scrollbar pt-36">
{galleries.map((gallery, index) => (
<GalleryThumbnail key={index} id={gallery.id} onSelect={() => selectGallery(gallery.id)}></GalleryThumbnail>
))}
</div>
</div>
</div>
{(isOpen ? (
<>
<div className={`fixed inset-0 transition-opacity${isOpen ? 'animate-in' : 'fade-out'}`} aria-hidden="true">
<div className="absolute inset-0 bg-neroshi-blue-900 opacity-70" onClick={()=>setIsOpen(false)} >
</div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-20 w-full p-20">
<Gallery id={selectedGallery as number} closeMenu={() => setIsOpen(false)}></Gallery>
</div>
</div>
</>
): null)}
</div>
) : (
<h1>loading</h1>
)));
}
export default PageComponent;

View File

@ -2,6 +2,40 @@
@tailwind components;
@tailwind utilities;
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.my-masonry-grid {
display: -webkit-box; /* Not needed if autoprefixing */
display: -ms-flexbox; /* Not needed if autoprefixing */
display: flex;
margin-left: -30px; /* gutter size offset */
width: auto;
}
.my-masonry-grid_column {
padding-left: 30px; /* gutter size */
background-clip: padding-box;
}
/* Style your items */
.my-masonry-grid_column > div { /* change div to reference your elements you put in <Masonry> */
background: grey;
margin-bottom: 30px;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.fade-out {
animation: fadeOut 0.5s forwards;
}
@layer base {
:root {
--background: 200 20% 98%;

View File

@ -1,6 +1,7 @@
import { GeistSans } from "geist/font/sans";
import "./globals.css";
import NavigationBar from "@/components/NavigationBar";
import { Vortex } from "@/components/ui/vortex";
const defaultUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
@ -23,7 +24,7 @@ export default function RootLayout({
<div className="w-full absolute z-20">
<NavigationBar/>
</div>
<main className="min-h-screen flex flex-col items-center">
<main className="min-h-screen flex flex-col items-center bg-gradient-to-r from-neroshi-blue-900 to-neroshi-blue-950">
{children}
</main>
</body>

33
app/livestream/page.tsx Normal file
View File

@ -0,0 +1,33 @@
import React, { useState, useEffect } from 'react';
function PageComponent() {
const getData = async () => {
}
return (
<div className="p-40 h-full w-full animate-in"> {/* This adds padding top of 20px */}
<div className="flex">
<iframe
className="flex-grow"
style={{flexBasis: '90%'}} // Video takes up 90% of the width
src="http://localhost:8080/embed/video"
title="Owncast"
height={720}
referrerPolicy="origin"
allowFullScreen
></iframe>
<iframe
className="flex-2"
style={{flexBasis: '10%'}} // Chat takes up 10% of the width
src="http://localhost:8080/embed/chat/readwrite"
title="Owncast"
referrerPolicy="origin"
allowFullScreen
></iframe>
</div>
</div>
);
}
export default PageComponent;

View File

@ -25,7 +25,7 @@ export default function Login({
return redirect("/login?message=Could not authenticate user");
}
return redirect("/protected");
return redirect("/gallery");
};
const signUp = async (formData: FormData) => {
@ -59,7 +59,7 @@ export default function Login({
<Link
href="/"
className="absolute left-1 top-44 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
className="absolute left-1 top-44 py-2 px-4 rounded-md no-underline text-foreground flex items-center group text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -93,21 +93,21 @@ export default function Login({
<div className="flex">
<SubmitButton
formAction={signIn}
className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2 mx-1 w-1/2"
className="bg-neroshi-blue-500 hover:bg-neroshi-blue-400 rounded-md px-4 py-2 text-foreground mb-2 mx-1 w-1/2"
pendingText="Signing In..."
>
Sign In
</SubmitButton>
<SubmitButton
formAction={signUp}
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2 mx-1 w-1/2"
className="bg-neroshi-blue-300 hover:bg-neroshi-blue-200 border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2 mx-1 w-1/2"
pendingText="Signing Up..."
>
Sign Up
</SubmitButton>
</div>
{searchParams?.message && (
<p className="mt-4 bg-foreground/10 mt-12 p-2 text-foreground text-center">
<p className="mt-4 bg-foreground/10 mt-14 p-2 text-foreground text-center">
{searchParams.message}
</p>
)}

View File

@ -1,4 +1,5 @@
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export default async function Index() {
const canInitSupabaseClient = () => {
@ -14,11 +15,5 @@ export default async function Index() {
const isSupabaseConnected = canInitSupabaseClient();
return (
<div className="flex-1 w-full flex flex-col gap-20 items-center animate-in">
<h1>This is unprotected.</h1>
</div>
// <div className="flex-1 w-full flex flex-col gap-20 items-center animate-in">
// </div>
);
return redirect("/gallery")
}

View File

@ -1,23 +0,0 @@
export default function DeployButton() {
return (
<a
className="py-2 px-3 flex rounded-md no-underline hover:bg-btn-background-hover border"
href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This%20starter%20configures%20Supabase%20Auth%20to%20use%20cookies%2C%20making%20the%20user's%20session%20available%20throughout%20the%20entire%20Next.js%20app%20-%20Client%20Components%2C%20Server%20Components%2C%20Route%20Handlers%2C%20Server%20Actions%20and%20Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6"
target="_blank"
rel="noreferrer"
>
<svg
aria-label="Vercel logomark"
role="img"
viewBox="0 0 74 64"
className="h-4 w-4 mr-2"
>
<path
d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
fill="currentColor"
></path>
</svg>
Deploy to Vercel
</a>
);
}

View File

@ -19,85 +19,103 @@ export default async function AuthButton() {
return redirect("/login");
};
if(user){
let email = user.email;
if(email != null){
const emailHash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex');
const gravatarUrl = `https://www.gravatar.com/avatar/${emailHash}`;
return(
<nav className="w-full flex justify-center h-16">
<div className="w-full max-w-2xl flex justify-between items-center p-3 text-sm">
<div className="flex items-center gap-2 ">
<Link
href="/gallery"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Gallery
</Link>
<Link
href="/commissions"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Commissions
</Link>
<Link
href="/subscriptions"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Subscription
</Link>
</div>
<div className="flex items-center gap-2">
<form action={signOut}>
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
Logout
</button>
</form>
<img src={gravatarUrl} alt="Profile" className="w-10 h-10 object-cover rounded-full cursor-pointer" />
</div>
</div>
</nav>)
<div className="flex justify-center items-center pt-2 ">
<nav className="w-1/3 bg-neroshi-blue-300 bg-opacity-10 flex justify-center h-16 animate-in rounded-3xl" style={{ backdropFilter: 'blur(10px)' }}>
<div className="w-full max-w-2xl flex justify-between items-center p-3 text-sm">
<div className="flex items-center gap-2 ">
<Link
href="/gallery"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Gallery
</Link>
<Link
href="/livestream"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Stream
</Link>
<Link
href="/commissions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Commissions
</Link>
<Link
href="/subscriptions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Subscription
</Link>
</div>
<div className="flex items-center gap-2">
<form action={signOut}>
<button className="py-2 px-4 rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800">
Logout
</button>
</form>
<img src={gravatarUrl} alt="Profile" className="w-10 h-10 object-cover rounded-full cursor-pointer" />
</div>
</div>
</nav>
</div>)
}
}
else{
return(
<nav className="w-full flex justify-center h-16">
<div className="w-full max-w-2xl flex justify-between items-center p-3 text-sm">
return( <div className="flex justify-center items-center pt-2 ">
<nav className="w-1/3 bg-neroshi-blue-300 bg-opacity-10 flex justify-center h-16 animate-in rounded-3xl" style={{ backdropFilter: 'blur(10px)' }}>
<div className="w-full max-w-2xl flex justify-between items-center p-3 text-sm">
<div className="flex items-center gap-2 ">
<Link
href="/gallery"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Gallery
</Link>
<Link
href="/commissions"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Commissions
</Link>
<Link
href="/subscriptions"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Subscription
</Link>
<Link
href="/gallery"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Gallery
</Link>
<Link
href="/livestream"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Stream
</Link>
<Link
href="/commissions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Commissions
</Link>
<Link
href="/subscriptions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Subscription
</Link>
</div>
<div className="flex items-center gap-2">
<Link
href="/login"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Login
</Link>
<Link
href="/login"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Signup
</Link>
</div>
</div>
</nav>)
</nav>
</div>)
}
}

20
components/ui/example.tsx Normal file
View File

@ -0,0 +1,20 @@
import { use, useState } from 'react';
import { useEffect } from 'react';
interface GalleryThumbnailProps {
}
const GalleryThumbnail = ({ }: GalleryThumbnailProps) => {
const getData = async () => {
}
useEffect(() => {
getData();
}, []);
return (
<>
</>
);
}
export default GalleryThumbnail;

129
components/ui/gallery.tsx Normal file
View File

@ -0,0 +1,129 @@
import { use, useState } from 'react';
import { useEffect } from 'react';
import Masonry from 'react-masonry-css';
interface GalleryProps {
id: number;
closeMenu: () => void;
}
const Gallery = ({ id, closeMenu }: GalleryProps) => {
const [isSingle, setIsSingle] = useState<boolean>(false);
const [loaded, setLoaded] = useState({})
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]);
const [galleryId, setGalleryId] = useState(id as number);
console.log(id)
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/'+String(galleryId)+'/images');
const thumbnailUrl = await thumbnailResponse.json() as string[];
setImages(thumbnailUrl);
}
const handleDownload = (image: string) => {
const link = document.createElement('a');
link.href = image;
link.download = 'image.jpg'; // or any other filename
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
useEffect(() => {
getData();
if (images.length === 1) {
setIsSingle(true);
setSelectedImage(images[0]);
}
}, [selectedImage]);
const handleClick = (image: string) => {
setSelectedImage(image);
};
const breakpointColumnsObj = {
default: 3
};
return (
<>
<button
className="fixed bg-purple-800 left-10 bottom-5 animate-shake mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50"
onClick={()=> closeMenu()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>{" "}
Back
</button>
<div className='z-10 pt-10' style={{ display: selectedImage ? 'flex' : 'block', alignItems: 'flex-start' }}>
{isSingle ? (
<div className='w-full h-full flex items-center'>
{selectedImage &&
<img
src={selectedImage}
style={{ objectFit: 'contain' }}
className="cursor-pointer animate-in w-full h-auto"
onClick={() => setSelectedImage(null)}
/>
}
</div>
) : (
<>
{selectedImage &&
<>
<img
src={selectedImage}
style={{ objectFit: 'contain' }}
className="cursor-pointer animate-in w-4/6 pr-20 h-auto"
onClick={() => setSelectedImage(null)}
/>
<button
className="fixed bg-neroshi-blue-800 left-40 bottom-5 animate-pulse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50"
onClick={() => handleDownload(selectedImage)}
>
<svg className='mr-3' xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download Current Image
</button>
</>
}
<Masonry
breakpointCols={selectedImage==null ? 4 : 2}
className="my-masonry-grid"
style={{ width: selectedImage ? '50%' : '100%' }}
>
{images.filter(img => img !== selectedImage).map((image, index) => (
<img
key={index}
src={image}
onClick={() => handleClick(image)}
className={`animate-in hover:scale-105 p-2 cursor-pointer my-2 transition-all opacity-100 duration-500 ease-in-out transform`}
/>
))}
</Masonry>
</>
)}
</div>
</>
);
}
export default Gallery;

View File

@ -0,0 +1,41 @@
import { use, useState } from 'react';
import { useEffect } from 'react';
interface GalleryThumbnailProps {
id: number;
onSelect: (id:number) => void;
}
const GalleryThumbnail = ({ id, onSelect }: GalleryThumbnailProps) => {
const [galleryId, setGalleryId] = useState(id as number);
const [thumbnailUrl, setThumbnailUrl] = useState('' as string);
const toggleModal = () => {
onSelect(galleryId);
};
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/'+galleryId+'/thumbnail');
const thumbnailUrl = await thumbnailResponse.text();
setThumbnailUrl(thumbnailUrl);
}
useEffect(() => {
getData();
}, []);
return (
<div className="py-3 sm:max-w-xl sm:mx-auto animate-in flex-3">
<div className="h-48 overflow-visible w-full relative hover:scale-105 shadow-lg">
<img
className={`aspect-content rounded-3xl`}
src={thumbnailUrl}
alt=""
onClick={toggleModal}
style={{ width: '20rem', height: '20rem', objectFit: 'cover' }}
/>
</div>
</div>
);
}
export default GalleryThumbnail;

255
components/ui/vortex.tsx Normal file
View File

@ -0,0 +1,255 @@
import { cn } from "@/utils/cn";
import React, { useEffect, useRef } from "react";
import { createNoise3D } from "simplex-noise";
import { motion } from "framer-motion";
interface VortexProps {
children?: any;
className?: string;
containerClassName?: string;
particleCount?: number;
rangeY?: number;
baseHue?: number;
baseSpeed?: number;
rangeSpeed?: number;
baseRadius?: number;
rangeRadius?: number;
backgroundColor?: string;
}
export const Vortex = (props: VortexProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef(null);
const particleCount = props.particleCount || 700;
const particlePropCount = 9;
const particlePropsLength = particleCount * particlePropCount;
const rangeY = props.rangeY || 100;
const baseTTL = 50;
const rangeTTL = 150;
const baseSpeed = props.baseSpeed || 0.0;
const rangeSpeed = props.rangeSpeed || 1.5;
const baseRadius = props.baseRadius || 1;
const rangeRadius = props.rangeRadius || 2;
const baseHue = props.baseHue || 220;
const rangeHue = 100;
const noiseSteps = 3;
const xOff = 0.00125;
const yOff = 0.00125;
const zOff = 0.0005;
const backgroundColor = props.backgroundColor || "#000000";
let tick = 0;
const noise3D = createNoise3D();
let particleProps = new Float32Array(particlePropsLength);
let center: [number, number] = [0, 0];
const HALF_PI: number = 0.5 * Math.PI;
const TAU: number = 2 * Math.PI;
const TO_RAD: number = Math.PI / 180;
const rand = (n: number): number => n * Math.random();
const randRange = (n: number): number => n - rand(2 * n);
const fadeInOut = (t: number, m: number): number => {
let hm = 0.5 * m;
return Math.abs(((t + hm) % m) - hm) / hm;
};
const lerp = (n1: number, n2: number, speed: number): number =>
(1 - speed) * n1 + speed * n2;
const setup = () => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (canvas && container) {
const ctx = canvas.getContext("2d");
if (ctx) {
resize(canvas, ctx);
initParticles();
draw(canvas, ctx);
}
}
};
const initParticles = () => {
tick = 0;
// simplex = new SimplexNoise();
particleProps = new Float32Array(particlePropsLength);
for (let i = 0; i < particlePropsLength; i += particlePropCount) {
initParticle(i);
}
};
const initParticle = (i: number) => {
const canvas = canvasRef.current;
if (!canvas) return;
let x, y, vx, vy, life, ttl, speed, radius, hue;
x = rand(canvas.width);
y = center[1] + randRange(rangeY);
vx = 0;
vy = 0;
life = 0;
ttl = baseTTL + rand(rangeTTL);
speed = baseSpeed + rand(rangeSpeed);
radius = baseRadius + rand(rangeRadius);
hue = baseHue + rand(rangeHue);
particleProps.set([x, y, vx, vy, life, ttl, speed, radius, hue], i);
};
const draw = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => {
tick++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawParticles(ctx);
renderGlow(canvas, ctx);
renderToScreen(canvas, ctx);
window.requestAnimationFrame(() => draw(canvas, ctx));
};
const drawParticles = (ctx: CanvasRenderingContext2D) => {
for (let i = 0; i < particlePropsLength; i += particlePropCount) {
updateParticle(i, ctx);
}
};
const updateParticle = (i: number, ctx: CanvasRenderingContext2D) => {
const canvas = canvasRef.current;
if (!canvas) return;
let i2 = 1 + i,
i3 = 2 + i,
i4 = 3 + i,
i5 = 4 + i,
i6 = 5 + i,
i7 = 6 + i,
i8 = 7 + i,
i9 = 8 + i;
let n, x, y, vx, vy, life, ttl, speed, x2, y2, radius, hue;
x = particleProps[i];
y = particleProps[i2];
n = noise3D(x * xOff, y * yOff, tick * zOff) * noiseSteps * TAU;
vx = lerp(particleProps[i3], Math.cos(n), 0.5);
vy = lerp(particleProps[i4], Math.sin(n), 0.5);
life = particleProps[i5];
ttl = particleProps[i6];
speed = particleProps[i7];
x2 = x + vx * speed;
y2 = y + vy * speed;
radius = particleProps[i8];
hue = particleProps[i9];
drawParticle(x, y, x2, y2, life, ttl, radius, hue, ctx);
life++;
particleProps[i] = x2;
particleProps[i2] = y2;
particleProps[i3] = vx;
particleProps[i4] = vy;
particleProps[i5] = life;
(checkBounds(x, y, canvas) || life > ttl) && initParticle(i);
};
const drawParticle = (
x: number,
y: number,
x2: number,
y2: number,
life: number,
ttl: number,
radius: number,
hue: number,
ctx: CanvasRenderingContext2D
) => {
ctx.save();
ctx.lineCap = "round";
ctx.lineWidth = radius;
ctx.strokeStyle = `hsla(${hue},100%,60%,${fadeInOut(life, ttl)})`;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.closePath();
ctx.restore();
};
const checkBounds = (x: number, y: number, canvas: HTMLCanvasElement) => {
return x > canvas.width || x < 0 || y > canvas.height || y < 0;
};
const resize = (
canvas: HTMLCanvasElement,
ctx?: CanvasRenderingContext2D
) => {
const { innerWidth, innerHeight } = window;
canvas.width = innerWidth;
canvas.height = innerHeight;
center[0] = 0.5 * canvas.width;
center[1] = 0.5 * canvas.height;
};
const renderGlow = (
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D
) => {
ctx.save();
ctx.filter = "blur(8px) brightness(200%)";
ctx.globalCompositeOperation = "lighter";
ctx.drawImage(canvas, 0, 0);
ctx.restore();
ctx.save();
ctx.filter = "blur(4px) brightness(200%)";
ctx.globalCompositeOperation = "lighter";
ctx.drawImage(canvas, 0, 0);
ctx.restore();
};
const renderToScreen = (
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D
) => {
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.drawImage(canvas, 0, 0);
ctx.restore();
};
useEffect(() => {
setup();
window.addEventListener("resize", () => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (canvas && ctx) {
resize(canvas, ctx);
}
});
}, []);
return (
<div className={cn("relative h-full w-full", props.containerClassName)}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
ref={containerRef}
className="absolute h-full w-full inset-0 z-0 bg-transparent flex items-center justify-center"
>
<canvas ref={canvasRef}></canvas>
</motion.div>
<div className={cn("relative z-10", props.className)}>
{props.children}
</div>
</div>
);
};

View File

@ -20,6 +20,7 @@ services:
restart: unless-stopped
ports:
- 8080:8080
- 1935:1935
volumes:
- ./data:/owncast/data

109
package-lock.json generated
View File

@ -7,13 +7,21 @@
"dependencies": {
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tailwindcss/aspect-ratio": "^0.4.2",
"autoprefixer": "10.4.17",
"clsx": "^2.1.1",
"framer-motion": "^11.2.6",
"geist": "^1.2.1",
"md5": "^2.3.0",
"next": "latest",
"postcss": "8.4.33",
"prop-types": "^15.8.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-masonry-css": "^1.0.16",
"react-responsive-masonry": "^2.2.0",
"simplex-noise": "^4.0.1",
"tailwind-merge": "^2.3.0",
"tailwindcss": "3.4.1",
"tailwindcss-animated": "^1.0.1",
"tailwindcss-textshadow": "^2.1.3",
@ -37,6 +45,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
"integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@fullhuman/postcss-purgecss": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-2.3.0.tgz",
@ -463,6 +482,14 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/aspect-ratio": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
"integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==",
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@types/node": {
"version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
@ -813,6 +840,14 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
@ -1070,6 +1105,30 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.6.tgz",
"integrity": "sha512-XUrjjBt57e5YoHQtjwc3eNchFBuHvIgN/cS8SC4oIaAn2J/0+bLanUxXizidJKZVeHJam/JrmMnPRjYMglVn5g==",
"dependencies": {
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@ -1925,6 +1984,16 @@
"node": ">= 0.8"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/purgecss": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-2.3.0.tgz",
@ -2121,6 +2190,24 @@
"react": "^18.2.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-masonry-css": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/react-masonry-css/-/react-masonry-css-1.0.16.tgz",
"integrity": "sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==",
"peerDependencies": {
"react": ">=16.0.0"
}
},
"node_modules/react-responsive-masonry": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-responsive-masonry/-/react-responsive-masonry-2.2.0.tgz",
"integrity": "sha512-IYbnfe2tWCZ3pvyTLyBWPj7uv5ZmNOULYMcAZi5a47ZLhSotOck1vkkISq6gP2qiyWdMvPfeMhjvYzUYGw9BOQ=="
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -2154,6 +2241,11 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -2253,6 +2345,11 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/simplex-noise": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.1.tgz",
"integrity": "sha512-zl/+bdSqW7HJOQ0oDbxrNYaF4F5ik0i7M6YOYmEoIJNtg16NpvWaTTM1Y7oV/7T0jFljawLgYPS81Uu2rsfo1A=="
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2430,6 +2527,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwind-merge": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
"integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==",
"dependencies": {
"@babel/runtime": "^7.24.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",

View File

@ -8,13 +8,21 @@
"dependencies": {
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tailwindcss/aspect-ratio": "^0.4.2",
"autoprefixer": "10.4.17",
"clsx": "^2.1.1",
"framer-motion": "^11.2.6",
"geist": "^1.2.1",
"md5": "^2.3.0",
"next": "latest",
"postcss": "8.4.33",
"prop-types": "^15.8.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-masonry-css": "^1.0.16",
"react-responsive-masonry": "^2.2.0",
"simplex-noise": "^4.0.1",
"tailwind-merge": "^2.3.0",
"tailwindcss": "3.4.1",
"tailwindcss-animated": "^1.0.1",
"tailwindcss-textshadow": "^2.1.3",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More