feat: tags

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

* feat: search components

* Update search.tsx

* fix: autofocus

* fix: tags and search
This commit is contained in:
Damien Ostler 2024-06-01 06:21:59 -04:00 committed by GitHub
parent 92c2317af1
commit f2bd76487e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 781 additions and 466 deletions

View File

@ -54,8 +54,8 @@ export async function GET(
.select('*') .select('*')
.eq('user_id', userId) .eq('user_id', userId)
.single(); .single();
console.log(subscription) //console.log(subscription)
console.log(gallery.tier) //console.log(gallery.tier)
switch(gallery.tier){ switch(gallery.tier){
case "Tier 3": case "Tier 3":
if(subscription?.tier!="Tier 3"){ if(subscription?.tier!="Tier 3"){

View File

@ -1,85 +1,22 @@
"use client"; "use client";
import { createClient } from "@/utils/supabase/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 React, { useState, useEffect } from 'react';
import { User } from "@supabase/supabase-js"; import Search from "@/components/neroshitron/search";
import Gallery from "@/components/ui/gallery";
function PageComponent() { function PageComponent() {
const supabase = createClient(); 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 () => { 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(() => { useEffect(() => {
getData(); 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 ( return (
<div> <div className="w-full">
<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"> <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 <img
src="gallery_girl.png" src="gallery_girl.png"
@ -87,114 +24,7 @@ function PageComponent() {
alt="Background" alt="Background"
/> />
</div> </div>
<Search/>
{/*
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> </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 { GeistSans } from "geist/font/sans";
import "./globals.css"; import "./globals.css";
import NavigationBar from "@/components/NavigationBar"; import NavigationBar from "@/components/neroshitron/navigation_bar";
import { SpeedInsights } from "@vercel/speed-insights/next" import { SpeedInsights } from "@vercel/speed-insights/next"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
const defaultUrl = process.env.VERCEL_URL 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,61 @@
"use client;"
import React, { useState, useEffect } from 'react';
import GalleryThumbnail from './gallery_thumbnail';
interface TagProps {
nsfw: boolean;
tags: string[];
search: string;
}
const Galleries = ({ nsfw, tags, search }:TagProps) => {
const [galleries, setGalleries] = useState([]);
const [nsfwState, setNsfwState] = useState<boolean>(nsfw);
const [tagsState, setTagsState] = useState<string[]>(tags);
const [searchState, setSearchState] = useState<string>(search);
const [selectedGallery, setSelectedGallery] = useState<string | null>(null);
const selectGallery = (gallery: string) => {
setSelectedGallery(gallery);
};
console.log(tags)
const getData = async () => {
const galleriesResponse = await fetch(`/api/galleries?search=` + searchState + '&nsfw=' + nsfwState, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags: tagsState })
});
const galleriesData = await galleriesResponse.json();
setGalleries(galleriesData);
}
useEffect(() => {
getData();
}, [tagsState]);
return (
<div className="absolute inset-0 mx-auto ml-16 md:ml-0 pt-48 p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-60 gap-x-4 animate-in overflow-y-scroll no-scrollbar z-0">
{galleries && galleries.map((gallery, index) => (
<GalleryThumbnail
key={gallery.name + " " + nsfw}
id={gallery.name}
title={gallery.name}
tags={gallery.tags}
columns={gallery.columns}
showNsfw={nsfw}
subscription={gallery.tier as string}
onSelect={selectGallery}
nsfw={gallery.nsfw}
></GalleryThumbnail>
))}
</div>
);
};
export default Galleries;

View File

@ -12,18 +12,11 @@ interface GalleryProps {
const Gallery = ({ id, columns, closeMenu }: 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 [selectedImage, setSelectedImage] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]); const [images, setImages] = useState<string[]>([]);
const [galleryId, setGalleryId] = useState(id as string); const [galleryId, setGalleryId] = useState(id as string);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const panZoomRef = useRef<any>(null);
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/' + String(galleryId) + '/images');
const thumbnailUrl = await thumbnailResponse.json() as string[];
setImages(thumbnailUrl);
}
const next = () => { const next = () => {
if (currentIndex < images.length - 1) { 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 = () => { const renderButtons = () => {
return ( return (
<div className="z-20 bottom-10 fixed pt-4 bg-purple-900 bg-opacity-40 animate-in rounded-2xl" style={{ backdropFilter: 'blur(10px)' }}> <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> </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 ( return (
<div > <div >
<div className="z-20" <div className="z-20"
onClick={resetPanZoom} style={{ width: selectedImage ? "100%" : "auto", height: selectedImage ? "100%" : "auto" }}> onClick={resetPanZoom} style={{ width: selectedImage ? "100%" : "auto", height: selectedImage ? "100%" : "auto" }}>
<div className='flex justify-center items-center pt-2 pb-20'> <div className='flex justify-center items-center pt-2 pb-20'>
{renderButtons()} {renderButtons()}
</div> </div>
{selectedImage ? ( {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>
<> <>
<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> </div>
); );
} }

View File

@ -0,0 +1,110 @@
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,42 @@
"use client;"
import React, { useState, useEffect } from 'react';
import SearchInput from '@/components/neroshitron/search_input';
import Galleries from './galleries';
interface SearchProps { }
const Search = ({ }:SearchProps) => {
const [tags, setTags] = useState<string[]>([]);
const [search, setSearch] = useState<string>('');
const [nsfw, setNsfw] = useState<boolean>(false);
const getData = async () => {
}
useEffect(() => {
getData();
}, [search]);
useEffect(() => {
getData();
}, [nsfw]);
useEffect(() => {
getData();
}, [tags]);
useEffect(() => {
getData();
}, []);
return (
<>
<Galleries key={search+"-"+tags.length+"-"+nsfw} search={search} nsfw={nsfw} tags={tags} />
<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 searchChanged={(search)=>{setSearch(search)}} nsfwChanged={(nsfw)=>{setNsfw(nsfw)}} tagsChanged={(tags)=>{setTags(tags);}} />
</div>
</section>
</>
);
};
export default Search;

View File

@ -0,0 +1,109 @@
"use client;"
import React, { useState, useEffect, useRef,forwardRef } from 'react';
import TagSelector from '../neroshitron/tag_selector';
interface SearchInputProps {
tagsChanged: (tags: string[]) => void;
searchChanged: (search: string) => void;
nsfwChanged: (nsfw: boolean) => void;
}
const SearchInput = ({ tagsChanged, searchChanged, nsfwChanged}: SearchInputProps) => {
const [search, setSearch] = useState<string>('');
const [tagSearch, setTagSearch] = 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 [tags, setTags] = useState<any[]>([]);
const updateTags = (newTags: string[]) => {
setSelectedTags(newTags)
}
const onTagsClosed = (tags:string[]) => {
setSelectingTags(false);
}
const openTags = () => {
setSelectingTags(true);
if(selectingTags){
onTagsClosed(selectedTags);
}
}
const getData = async () => {
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setTags(tagsData);
}
useEffect(() => {
searchChanged(search);
}, [search]);
useEffect(() => {
tagsChanged(selectedTags);
}, [selectedTags]);
useEffect(() => {
nsfwChanged(nsfw);
}, [nsfw]);
useEffect(() => {
getData();
}, []);
return (
<>
<div className="relative md:w-full lg:w-1/2 mx-auto flex flex-col items-center justify-center z-10">
<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">
{(selectingTags) ? (
<>
<input autoFocus value={tagSearch} onChange={(e) => setTagSearch(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 specific tag?" />
<span className="flex items-center bg-gray-100 rounded rounded-l-none border-0 px-3 font-bold text-grey-100">
<button key="back" onClick={()=>{openTags()}} type="button" className={`animate-in bg-pink-900 hover:bg-pink-800 text-lg text-white font-bold py-3 px-6 rounded`}>
Back
</button>
</span>
</>
)
:(
<>
<input autoFocus 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 key="tags" data-tip={selectedTags.join(',')} 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' : 'animate-in'}`}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6h.008v.008H6V6Z" />
</svg>
</button>
<button
onClick={()=>{ setNsfw(!nsfw) }}
type="button"
className={`animate-in text-sm w-28 text-lg text-white font-bold py-3 px-6 rounded ml-2 ${nsfw ? "bg-pink-900 hover:bg-pink-800":"bg-green-900 hover:bg-green-800"}`}
>
{nsfw ? "NSFW" : "SFW"}
</button>
</span>
</>
)}
</div>
</div>
</div>
{(selectingTags) &&
<TagSelector tagsInput={tags} key={tagSearch} tagSearch={tagSearch} tagsChanged={(newTags:string[])=>{ updateTags(newTags) }} selectedTagsInput={selectedTags} ref={tagSelectorRef} />}
</>
);
};
export default SearchInput;

View File

@ -0,0 +1,19 @@
"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 (
<button
type="button"
className={`animate-in w-full h-8 rounded-md no-underline text-sm text-white py-1 font-medium text-center ${selected ? 'hover:bg-pink-800 bg-pink-900' : 'hover:bg-pink-600 bg-neroshi-blue-800 border-neroshi-blue-900 border-2'}`}
onClick={() => onTagClicked(tag)}
>
{tag}
</button>
);
};
export default Tag;

View File

@ -0,0 +1,60 @@
"use client;"
import React, { forwardRef, useState, useEffect } from 'react';
import Tag from './tag_pill';
interface TagSelectorProps {
tagsInput: any[],
tagSearch: string,
selectedTagsInput: string[],
tagsChanged: (tags: string[]) => void
}
const TagSelector = forwardRef<TagSelectorProps, {tagsInput:any[], tagSearch: string, selectedTagsInput: string[], tagsChanged: (tags: string[]) => void }>((props, ref) => {
const [tags, setTags] = useState<any[]>(props.tagsInput);
const [tagSearch, setTagSearch] = useState<string>(props.tagSearch);
const [selectedTags, setSelectedTags] = useState<string[]>(props.selectedTagsInput);
console.log()
const handleTag = (tag: string) => {
if (selectedTags.includes(tag)) {
setSelectedTags(selectedTags.filter(t => t !== tag));
} else {
setSelectedTags([...selectedTags, tag]);
}
setTags(selectedTags);
};
const getData = async () => {
}
useEffect(() => {
props.tagsChanged(selectedTags);
getData();
}, [selectedTags,tagSearch,tags]);
const generateRandomString = () => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 10; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
};
return (
(tags.length > 0)? (
<div className="animate-in flex md:w-full animate-in pt-4 justify-center items-center">
<div className="z-10 grid p-4 grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-1 w-full h-max-72 overflow-y-scroll no-scrollbar pt-4 bg-neroshi-blue-900 rounded-md opacity-90 backdrop-filter backdrop-blur-md mx-auto">
{props.tagsInput.map((tag: any) => (
(tagSearch === '' || tag.name.toLowerCase().includes(tagSearch.toLowerCase())) && // Updated condition
<Tag key={generateRandomString()} tag={tag.name} selected={selectedTags.includes(tag.name)} onTagClicked={(tag) => handleTag(tag)} />
))}
</div>
</div>
):(<></>)
);
});
export default TagSelector;

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;