fix: navigation bar code is minimized, started seperating out components and refactoring them

This commit is contained in:
Damien Ostler 2024-06-01 00:32:40 -04:00
parent 92c2317af1
commit 22df3ab0fd
12 changed files with 650 additions and 465 deletions

View File

@ -7,194 +7,23 @@ 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";
import Search from "@/components/neroshitron/search";
function PageComponent() {
const supabase = createClient();
const [showNSFW, setShowNSFW] = useState<boolean>(true);
const [randomIds, setRandomIds] = useState<string[]>([]); // replace any with your gallery type
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<string | null>(null);
const [tags, setTags] = useState<any[]>([]);
const [search, setSearch] = useState<string>('');
const [galleryColumns, setColumns] = useState<number>(0);
const [selectedTags, setSelectedTags] = useState<number[]>([]);
const generateRandomString = function (length: number) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const selectGallery = (gallery: string, columns: number) => {
setRandomIds([generateRandomString(3), generateRandomString(3), generateRandomString(3), generateRandomString(3)]);
setSelectedGallery(gallery);
setColumns(columns);
setIsOpen(true);
};
const closeGallery = () => {
setSelectedGallery(null);
setColumns(0);
setIsOpen(false);
}
const getData = async () => {
let { data: { user } } = await supabase.auth.getUser();
const galleriesResponse = await fetch(`/api/galleries?search=` + search + '&nsfw=' + showNSFW, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags: selectedTags })
});
//console.log(galleriesResponse)
const galleriesData = await galleriesResponse.json();
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setGalleries(galleriesData);
setTags(tagsData);
setUser(user);
setLoading(false);
}
useEffect(() => {
getData();
}, []);
}, [selectedTags, search, showNSFW]);
const handleTagClick = (tag: number) => {
if (selectedTags.includes(tag)) {
setSelectedTags(selectedTags.filter((selectedTag) => selectedTag !== tag));
} else {
setSelectedTags([...selectedTags, tag]);
}
//console.log(selectedTags)
};
return (
<div>
<div className="fixed w-full h-full overflow-hidden z-0 animate-fade-left animate-fade-left animate-once animate-duration-[2000ms] animate-normal animate-fill-forwards">
<img
src="gallery_girl.png"
className="float-right object-cover h-screen w-full lg:w-5/6 xl:w-3/6 opacity-50 overflow-hidden"
alt="Background"
/>
</div>
{/*
THIS IS THE SEARCH BAR AND TAGS SECTION
THIS IS THE SEARCH BAR AND TAGS SECTION
THIS IS THE SEARCH BAR AND TAGS SECTION
THIS IS THE SEARCH BAR AND TAGS SECTION
THIS IS THE SEARCH BAR AND TAGS SECTION
THIS IS THE SEARCH BAR AND TAGS SECTION
*/}
<section className="flex items-center w-full p-8 pt-20 opacity-90 animate-jump-in animate-once animate-duration-500">
{(tags.length > 0) ? (
<div className="container mx-auto py-8">
<div className="relative w-full mx-auto">
<input
className="w-full text-neroshi-blue-950 h-16 px-3 rounded mb-8 focus:outline-none focus:shadow-outline text-xl px-8 shadow-lg"
type="search"
placeholder="Search by title..."
onChange={(e) => setSearch(e.target.value)}
style={{
animation: 'expandFromLeft 2s ease-out forwards',
paddingRight: '2rem', // make room for the checkbox
}}
/>
<label htmlFor="toggleNSFW" className="absolute right-0 top-0 h-full mr-2 flex items-center text-neroshi-blue-950 animate-fade animate-once animate-duration-500 animate-delay-[2000ms] animate-ease-out">
Censor NSFW
<input
id="toggleNSFW"
type="checkbox"
checked={showNSFW}
onChange={() => setShowNSFW(!showNSFW)}
className="form-checkbox h-5 w-5 text-neroshi-blue-950 ml-2 "
/>
</label>
</div>
{(tags.length > 0) ? (
<nav className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-4 justify-items-center">
{tags.map((tag, index) => (
<a
key={index}
className={`w-full rounded-lg no-underline text-white py-3 px-4 font-medium text-center animate-jump-in animate-once animate-duration-500 animate-ease-out ${selectedTags.includes(tag.name) ? 'bg-neroshi-blue-950 hover:bg-neroshi-blue-900' : 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700'
}`}
href="#"
onClick={() => handleTagClick(tag.name)}
>
{tag.name}
</a>
))}
</nav>
) : (
<div className="flex justify-center">
<p className="text-white">Loading Tags...</p>
</div>
)}
</div>
) : (
<div className="animate-pulse bg-neroshi-blue-950 rounded-3xl w-full p-8 mt-10 h-48" ></div>
)}
</section>
{/*
These are the thumbnails for the gallery below the search bar
*/}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-y-60 gap-x-5 h-full mb-96 animate-in">
{galleries && galleries.map((gallery, index) => (
<div className="mx-auto">
<GalleryThumbnail
key={gallery.name + " " + showNSFW}
id={gallery.name}
title={gallery.name}
tags={gallery.tags}
columns={gallery.columns}
showNsfw={showNSFW}
subscription={gallery.tier as string}
onSelect={selectGallery}
nsfw={gallery.nsfw}
></GalleryThumbnail>
</div>
))}
</div>
{isOpen ? (
<>
{/*
This is the modal for holding the gallery
*/}
<div
className={`fixed inset-0 transition-opacity z-30 ${isOpen ? 'animate-in' : 'fade-out'
}`}
aria-hidden="true"
>
<div
className="absolute inset-0 bg-neroshi-blue-900 opacity-70 z-30"
onClick={() => setIsOpen(false)}
></div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-2 w-full p-20 z-30">
<Gallery
id={selectedGallery as string}
columns={galleryColumns}
closeMenu={() => closeGallery()}
></Gallery>
</div>
</div>
</>
) : null}
<div className="w-full">
<Search/>
</div>
);
}

131
app/gallery/page_old.tsx Normal file
View File

@ -0,0 +1,131 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import { redirect } from "next/navigation";
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";
import Search from "@/components/ui/search";
function PageComponent() {
const supabase = createClient();
const [showNSFW, setShowNSFW] = useState<boolean>(true);
const [randomIds, setRandomIds] = useState<string[]>([]); // replace any with your gallery type
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<string | null>(null);
const [search, setSearch] = useState<string>('');
const [galleryColumns, setColumns] = useState<number>(0);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const generateRandomString = function (length: number) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const selectGallery = (gallery: string, columns: number) => {
setRandomIds([generateRandomString(3), generateRandomString(3), generateRandomString(3), generateRandomString(3)]);
setSelectedGallery(gallery);
setColumns(columns);
setIsOpen(true);
};
const closeGallery = () => {
setSelectedGallery(null);
setColumns(0);
setIsOpen(false);
}
const getData = async () => {
let { data: { user } } = await supabase.auth.getUser();
const galleriesResponse = await fetch(`/api/galleries?search=` + search + '&nsfw=' + showNSFW, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags: selectedTags })
});
const galleriesData = await galleriesResponse.json();
setGalleries(galleriesData);
setUser(user);
setLoading(false);
}
useEffect(() => {
getData();
}, [selectedTags, search, showNSFW]);
return (
<div>
<div className="fixed w-full h-full overflow-hidden z-0 animate-fade-left animate-fade-left animate-once animate-duration-[2000ms] animate-normal animate-fill-forwards">
<img
src="gallery_girl.png"
className="float-right object-cover h-screen w-full lg:w-5/6 xl:w-3/6 opacity-50 overflow-hidden"
alt="Background"
/>
</div>
<Search
/>
{/*
These are the thumbnails for the gallery below the search bar
*/}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-y-60 gap-x-5 h-full mb-96 animate-in">
{galleries && galleries.map((gallery, index) => (
<div className="mx-auto">
<GalleryThumbnail
key={gallery.name + " " + showNSFW}
id={gallery.name}
title={gallery.name}
tags={gallery.tags}
columns={gallery.columns}
showNsfw={showNSFW}
subscription={gallery.tier as string}
onSelect={selectGallery}
nsfw={gallery.nsfw}
></GalleryThumbnail>
</div>
))}
</div>
{isOpen ? (
<>
{/*
This is the modal for holding the gallery
*/}
<div
className={`fixed inset-0 transition-opacity z-30 ${isOpen ? 'animate-in' : 'fade-out'
}`}
aria-hidden="true"
>
<div
className="absolute inset-0 bg-neroshi-blue-900 opacity-70 z-30"
onClick={() => setIsOpen(false)}
></div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-2 w-full p-20 z-30">
<Gallery
id={selectedGallery as string}
columns={galleryColumns}
closeMenu={() => closeGallery()}
></Gallery>
</div>
</div>
</>
) : null}
</div>
);
}
export default PageComponent;

View File

@ -1,6 +1,6 @@
import { GeistSans } from "geist/font/sans";
import "./globals.css";
import NavigationBar from "@/components/NavigationBar";
import NavigationBar from "@/components/neroshitron/navigation_bar";
import { SpeedInsights } from "@vercel/speed-insights/next"
import { Analytics } from "@vercel/analytics/react"
const defaultUrl = process.env.VERCEL_URL

View File

@ -1,150 +0,0 @@
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect, useRouter } from "next/navigation";
import crypto from 'crypto';
import { headers } from "next/headers";
export default async function AuthButton() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const signOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/login");
};
// ...
const heads = headers()
const currentPage = heads.get('x-path')
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(
<div className="flex justify-center items-center pt-2 ">
<nav className="w-auto bg-neroshi-blue-300 bg-opacity-10 flex justify-center z-10 h-16 animate-in rounded-md" 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 z-10">
<Link
href="/gallery"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Gallery</span>
</Link>
<Link
href="/livestream"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Livestream</span>
</Link>
<Link
href="/commissions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 7.5h12.974c.576 0 1.059.435 1.119 1.007ZM8.625 10.5a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm7.5 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Commissions</span>
</Link>
<Link
href="/subscriptions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Subscription</span>
</Link>
</div>
<div className="flex items-center gap-2">
<form action={signOut}>
<button className="py-2 px-4 ml-2 rounded-md no-underline bg-pink-950 hover:bg-pink-900">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 md:hidden ">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Logout</span>
</button>
</form>
<img src={gravatarUrl} alt="Profile" className="w-10 h-10 object-cover rounded-full cursor-pointer" />
</div>
</div>
</nav>
</div>)
}
}
else{
return( <div className="flex justify-center items-center pt-2 ">
<nav className="w-auto bg-neroshi-blue-300 bg-opacity-10 flex justify-center h-16 animate-in rounded-md" 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 ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Gallery</span>
</Link>
<Link
href="/livestream"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Livestream</span>
</Link>
<Link
href="/commissions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 7.5h12.974c.576 0 1.059.435 1.119 1.007ZM8.625 10.5a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm7.5 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Commissions</span>
</Link>
<Link
href="/subscriptions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Subscription</span>
</Link>
<Link
href="/subscriptions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Login</span>
</Link>
</div>
</div>
</nav>
</div>)
}
}

View File

@ -0,0 +1,114 @@
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect, useRouter } from "next/navigation";
import crypto from 'crypto';
import { headers } from "next/headers";
export default async function AuthButton() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const signOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/login");
};
// ...
const heads = headers()
const currentPage = heads.get('x-path')
const getGravatarUrl = () => {
if(user==null)
{
return;
}
let email = user.email;
if(email != null){
const emailHash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex');
return `https://www.gravatar.com/avatar/${emailHash}`;
}
return "";
}
const url = getGravatarUrl();
return(
<div className="flex justify-center items-center pt-2 ">
<nav className="w-auto bg-neroshi-blue-300 bg-opacity-10 flex justify-center z-10 h-16 animate-in rounded-md" 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 z-10">
<Link
href="/gallery"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Gallery</span>
</Link>
<Link
href="/livestream"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Livestream</span>
</Link>
<Link
href="/commissions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 7.5h12.974c.576 0 1.059.435 1.119 1.007ZM8.625 10.5a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm7.5 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Commissions</span>
</Link>
<Link
href="/subscriptions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Subscription</span>
</Link>
</div>
<div className="flex items-center gap-2">
{(user!=null) ? (
<>
<form action={signOut}>
<button className="py-2 px-4 ml-2 rounded-md no-underline bg-pink-950 hover:bg-pink-900">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 md:hidden ">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Logout</span>
</button>
</form>
<img src={url} alt="Profile" className="w-10 h-10 object-cover rounded-full cursor-pointer" />
</>
) : (
<Link
href="/subscriptions"
className={`ml-2 py-2 px-3 flex rounded-md no-underline ${currentPage === '/gallery' ? 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700' : 'bg-neroshi-blue-900 hover:bg-neroshi-blue-800'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Login</span>
</Link>
)}
</div>
</div>
</nav>
</div>)
}

View File

@ -0,0 +1,30 @@
"use client;"
import React, { useState, useEffect } from 'react';
import Tag from "@/components/neroshitron/tag_pill";
import SearchInput from '@/components/neroshitron/search_input';
interface SearchProps { }
const Search = ({ }:SearchProps) => {
const [tags, setTags] = useState<any[]>([]);
const [selectingTags, setSelectingTags] = useState<boolean>(false);
const getData = async () => {
}
useEffect(() => {
getData();
}, []);
return (
<>
<section className="fixed flex items-center w-full p-8 pt-20 opacity-90 animate-in animate-once animate-duration-500">
<div className="container mx-auto py-8">
<SearchInput />
</div>
</section>
</>
);
};
export default Search;

View File

@ -0,0 +1,56 @@
"use client;"
import React, { useState, useEffect, useRef,forwardRef } from 'react';
import { on } from 'events';
import TagSelector from '../neroshitron/tag_selector';
interface SearchInputProps {
}
const SearchInput = ({ }: SearchInputProps) => {
const [search, setSearch] = useState<string>('');
const [nsfw, setNsfw] = useState<boolean>(false);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectingTags, setSelectingTags] = useState<boolean>(false);
const tagSelectorRef = React.useRef(null);
const onTagsClosed = (tags:string[]) => {
if(tagSelectorRef.current !== null){
setSelectedTags((tagSelectorRef.current as any).getSelectedTags());
}
setSelectingTags(false);
setSelectedTags(tags);
}
const openTags = () => {
setSelectingTags(true);
if(selectingTags){
onTagsClosed(selectedTags);
}
}
return (
<>
<div className="relative md:w-full lg:w-1/2 mx-auto flex flex-col items-center justify-center">
<div className="search-box mx-auto my-auto w-full sm:w-full md:w-full lg:w-3/4 xl:w-3/4">
<div className="flex flex-row">
<input value={search} onChange={(e) => setSearch(e.target.value)} className="rounded-l-md h-16 bg-gray-100 text-grey-darker py-2 font-normal text-grey-darkest border border-gray-100 font-bold w-full py-1 px-2 outline-none text-lg text-gray-600" type="text" placeholder="Looking for a specific collection?" />
<span className="flex items-center bg-gray-100 rounded rounded-l-none border-0 px-3 font-bold text-grey-100">
<button onClick={()=>{openTags()}} type="button" className={`bg-neroshi-blue-900 hover:bg-neroshi-blue-800 text-lg text-white font-bold py-3 px-6 rounded ${selectedTags.length === 0 ? 'animate-pulse animate-infinite animate-ease-out' : ''}`}>
Tags
<span className="ml-1 bg-neroshi-blue-300 text-white rounded-full px-2 py-1 text-xs absolute bottom-7 right-76 transform translate-x-1/2 -translate-y-1/2">{selectedTags.length}</span>
</button>
<button onClick={()=>{ setNsfw(!nsfw) }} type="button" className={` w-full ${nsfw ? "bg-pink-900 hover:bg-pink-800":"bg-green-900 hover:bg-green-800"} text-lg text-white font-bold py-3 px-6 rounded ml-6`}>
{nsfw ? "NSFW" : "SFW"}
</button>
</span>
</div>
</div>
</div>
{(selectingTags) && <TagSelector ref={tagSelectorRef} />}
</>
);
};
export default SearchInput;

View File

@ -0,0 +1,20 @@
"use client;"
import React, { useState, useEffect } from 'react';
interface TagProps { onTagClicked: (tag: string ) => void, selected:boolean, tag:string }
const Tag = ({ onTagClicked, selected, tag, }:TagProps) => {
return (
<a
key={"select-tags"}
className={`w-full m-4 rounded-md no-underline text-white py-1 font-medium text-center ${selected ? 'hover:bg-pink-800 bg-pink-900' : 'hover:bg-neroshi-blue-700 bg-neroshi-blue-800'}`}
href="#"
onClick={() => onTagClicked(tag)}
>
{tag}
</a>
);
};
export default Tag;

View File

@ -0,0 +1,52 @@
"use client;"
import React, { useImperativeHandle,forwardRef, useState, useEffect } from 'react';
import { createClient } from "@/utils/supabase/client";
import Tag from './tag_pill';
import Masonry from 'react-masonry-css';
interface TagSelectorProps {
}
const TagSelector = forwardRef<TagSelectorProps, {}>((props, ref) => {
const [tags, setTags] = useState<any[]>([]);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const supabase = createClient();
useImperativeHandle(ref, () => ({
getSelectedTags: () => {
return tags.map(tag => tag.name);
},
}));
const getData = async () => {
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setTags(tagsData);
}
const handleTag = (tag: string) => {
if (selectedTags.includes(tag)) {
setSelectedTags(selectedTags.filter(t => t !== tag));
} else {
setSelectedTags([...selectedTags, tag]);
}
};
useEffect(() => {
getData();
}, []);
console.log(selectedTags)
return (
<div className="flex md:w-full lg:w-1/2 animate-in mx-auto pt-4">
<div className="grid grid-cols-6 gap-4 w-full animate-in mx-auto pt-4 bg-neroshi-blue-900 pr-8 pb-4 rounded-md">
{tags.map((tag: any) => (
<Tag key={tag.id} tag={tag.name} selected={selectedTags.includes(tag.name)} onTagClicked={(tag) => handleTag(tag)} />
))}
</div>
</div>
);
});
export default TagSelector;

View File

@ -12,18 +12,11 @@ interface GalleryProps {
const Gallery = ({ id, columns, 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 string);
const [currentIndex, setCurrentIndex] = useState(0);
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/' + String(galleryId) + '/images');
const thumbnailUrl = await thumbnailResponse.json() as string[];
setImages(thumbnailUrl);
}
const panZoomRef = useRef<any>(null);
const next = () => {
if (currentIndex < images.length - 1) {
@ -41,6 +34,76 @@ const Gallery = ({ id, columns, closeMenu }: GalleryProps) => {
}
}
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);
};
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/' + String(galleryId) + '/images');
const thumbnailUrl = await thumbnailResponse.json() as string[];
setImages(thumbnailUrl);
}
useEffect(() => {
getData();
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowLeft':
case 'a':
case 'A':
previous();
break;
case 'ArrowRight':
case 'd':
case 'D':
next();
break;
case 'Escape':
close();
break;
default:
break;
}
};
setSelectedImage(images[currentIndex]);
window.addEventListener('keydown', handleKeyDown);
// Clean up the event listener when the component is unmounted
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedImage, currentIndex]);
const handleClick = (image: string) => {
setSelectedImage(image);
setCurrentIndex(images.indexOf(image));
};
const resetPanZoom = (event: any) => {
if (panZoomRef.current && event.target.id != "image-container") {
panZoomRef.current.autoCenter();
}
};
const close = () => {
if (selectedImage != null) {
setSelectedImage(null);
setImages([]);
}
else {
closeMenu();
}
};
const renderButtons = () => {
return (
<div className="z-20 bottom-10 fixed pt-4 bg-purple-900 bg-opacity-40 animate-in rounded-2xl" style={{ backdropFilter: 'blur(10px)' }}>
@ -85,142 +148,59 @@ const Gallery = ({ id, columns, closeMenu }: GalleryProps) => {
</div>
);
};
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();
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowLeft':
case 'a':
case 'A':
previous();
break;
case 'ArrowRight':
case 'd':
case 'D':
next();
break;
case 'Escape':
close();
break;
default:
break;
}
};
setSelectedImage(images[currentIndex]);
window.addEventListener('keydown', handleKeyDown);
// Clean up the event listener when the component is unmounted
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedImage,currentIndex]);
const handleClick = (image: string) => {
setSelectedImage(image);
setCurrentIndex(images.indexOf(image));
};
const open = () => {
if (selectedImage === null) return;
//console.log(selectedImage)
let base64Image = selectedImage.split(';base64,').pop();
if (!base64Image) return;
let blob = new Blob([Uint8Array.from(atob(base64Image), c => c.charCodeAt(0))], { type: 'image/jpeg' }); // adjust the type as needed
let url = URL.createObjectURL(blob);
window.open(url, '_blank');
}
const panZoomRef = useRef<any>(null);
const resetPanZoom = (event: any) => {
if (panZoomRef.current && event.target.id != "image-container") {
panZoomRef.current.autoCenter();
}
};
const close = () => {
if (selectedImage != null) {
setSelectedImage(null);
setImages([]);
}
else {
closeMenu();
}
}
const breakpointColumnsObj = {
default: 3
};
return (
<div >
<div className="z-20"
onClick={resetPanZoom} style={{ width: selectedImage ? "100%" : "auto", height: selectedImage ? "100%" : "auto" }}>
<div className='flex justify-center items-center pt-2 pb-20'>
{renderButtons()}
<div className="z-20"
onClick={resetPanZoom} style={{ width: selectedImage ? "100%" : "auto", height: selectedImage ? "100%" : "auto" }}>
<div className='flex justify-center items-center pt-2 pb-20'>
{renderButtons()}
</div>
{selectedImage ? (
<>
<PanZoom
key={selectedImage}
autoCenter={true}
ref={panZoomRef}
>
{/*
<div
onClick={() => resetPanZoom()} className='w-full h-full z-10'>
</div> */}
<div id="image-container" >
<img
src={images[currentIndex]}
style={{ objectFit: "contain", maxWidth: "100%", maxHeight: "calc(100vh - 20px)", pointerEvents:"none" }}
className="cursor-pointer animate-in w-full h-auto"
>
</img>
</div>
</PanZoom>
</>
) : (
<div
className="z-30"
style={{
display: selectedImage ? "flex" : "block",
alignItems: "flex-start",
}}
> <div className='flex justify-center items-center pt-2 '>
<Masonry
breakpointCols={columns}
className="my-masonry-grid pl-6 "
style={{ width: selectedImage ? "50%" : "100%" }}
>
{images
.filter((img) => img !== selectedImage)
.map((image, index) => (
<img
src={image}
onClick={() => handleClick(image)}
className={`animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse hover:scale-105 p-2 cursor-pointer my-2 transition-all opacity-100 duration-500 ease-in-out transform`}
/>
))}
</Masonry>
</div>
{selectedImage ? (
<>
<PanZoom
key={selectedImage}
autoCenter={true}
ref={panZoomRef}
>
<div id="image-container" >
<img
src={images[currentIndex]}
style={{ objectFit: "contain", maxWidth: "100%", maxHeight: "calc(100vh - 20px)", pointerEvents: "none" }}
className="cursor-pointer animate-in w-full h-auto"
>
</img>
</div>
</PanZoom>
</>
</div>
)}
</div>
) : (
<div
className="z-30"
style={{
display: selectedImage ? "flex" : "block",
alignItems: "flex-start",
}}
> <div className='flex justify-center items-center pt-2 '>
<Masonry
breakpointCols={columns}
className="my-masonry-grid pl-6 "
style={{ width: selectedImage ? "50%" : "100%" }}
>
{images
.filter((img) => img !== selectedImage)
.map((image, index) => (
<img
src={image}
onClick={() => handleClick(image)}
className={`animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse hover:scale-105 p-2 cursor-pointer my-2 transition-all opacity-100 duration-500 ease-in-out transform`}
/>
))}
</Masonry>
</div>
</div>
)}
</div>
</div>
);
}

96
components/ui/search.tsx Normal file
View File

@ -0,0 +1,96 @@
"use client;"
import React, { useState, useEffect } from 'react';
import TagSelector from './tag_selector';
interface SearchProps { }
const Search = ({ }:SearchProps) => {
const [nsfw, setNsfw] = useState<boolean>(false);
const [tags, setTags] = useState<any[]>([]);
const [search, setSearch] = useState<string>('');
const [selectedTags, setSelectedTagsState] = useState<string[]>([]);
const [selectingTags, setSelectingTags] = useState<boolean>(false);
const getNsfw = () => {
return nsfw;
};
const getTags = () => {
return tags;
};
const getSearch = () => {
return search;
};
const getSelectedTags = () => {
return selectedTags;
};
const getData = async () => {
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setTags(tagsData);
}
useEffect(() => {
getData();
}, [selectingTags]);
return (
<>
<section className="flex items-center w-full p-8 pt-20 opacity-90 animate-jump-in animate-once animate-duration-500">
{(tags.length > 0) ? (
<div className="container mx-auto py-8">
<div className="relative w-full mx-auto">
<input
className="animate-in animate-delay-[2000ms] w-full text-neroshi-blue-950 h-16 px-3 rounded mb-8 focus:outline-none focus:shadow-outline text-xl px-8 shadow-lg"
type="search"
placeholder="Search by title..."
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute right-0 top-0 h-full mr-2 flex items-center">
<label htmlFor="toggleNSFW" className="text-neroshi-blue-950 animate-in animate-delay-[2000ms] animate-ease-out">
Censor NSFW
<input
id="toggleNSFW"
type="checkbox"
onChange={(e) => setNsfw(e.target.checked)}
className="form-checkbox h-5 w-5 text-neroshi-blue-950 ml-2"
/>
</label>
</div>
</div>
<nav className="pt-6 grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-4 justify-items-center">
{tags.map((tag, index) => (
<a
key={index}
className={`w-full rounded-lg no-underline text-white py-3 px-4 font-medium text-center animate-jump-in animate-once animate-duration-500 animate-ease-out ${selectedTags.includes(tag.name) ? 'bg-neroshi-blue-950 hover:bg-neroshi-blue-900' : 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700'
}`}
href="#"
>
{tag.name}
</a>
))}
<a
key={"select-tags"}
className={`w-full rounded-lg no-underline text-white py-3 px-4 font-medium text-center animate-jump-in animate-once animate-duration-500 animate-ease-out bg-pink-800 hover:bg-pink-700`}
href="#"
onClick={() => setSelectingTags(true)}
>
Select Tags : {selectingTags}
</a>
</nav>
</div>
) : (
<div className="animate-pulse bg-neroshi-blue-950 rounded-3xl w-full p-8 mt-10 h-48"></div>
)}
{(selectingTags) ??(
<TagSelector/>
)}
</section>
</>
);
};
export default Search;

View File

@ -0,0 +1,27 @@
"use client;"
import React, { useState, useEffect } from 'react';
interface SearchProps { }
const TagSelector = ({ }:SearchProps) => {
const getData = async () => {
}
useEffect(() => {
getData();
}, []);
return (
<div className={`fixed inset-0 transition-opacity z-30 animate-in`} aria-hidden="true" >
<div className="absolute inset-0 bg-neroshi-blue-900 opacity-70 z-30">
</div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-2 w-full p-20 z-30">
Test
</div>
</div>
);
};
export default TagSelector;