Compare commits
8 Commits
ae7c821562
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e829b6ff5 | |||
| 6cda6fee45 | |||
| 0157a63201 | |||
| 1ee4043c0e | |||
| 2fd088019e | |||
| 2c44abc320 | |||
| ff5dc072a0 | |||
| bfc9b9a367 |
@@ -2,9 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>NWaifu Tier Maker</title>
|
||||
<script
|
||||
defer
|
||||
src="https://stats.nwaifu.su/script.js"
|
||||
data-website-id="2167decc-21d6-4f30-bdf3-f00da4b74ee9"
|
||||
data-domains="tiermaker.nwaifu.su"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nwtierlist",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^6.3.1",
|
||||
"@mui/material": "^6.3.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"zustand": "^5.0.3"
|
||||
|
||||
94
src/App.tsx
94
src/App.tsx
@@ -1,4 +1,6 @@
|
||||
import { DndContext, DragEndEvent } from '@dnd-kit/core';
|
||||
import { GitHub } from '@mui/icons-material';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { useRef } from 'react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import './App.scss';
|
||||
@@ -14,6 +16,8 @@ const App = () => {
|
||||
useShallow(state => [state.images, state.addTierImage, state.tierLevels, state.editTierImage]),
|
||||
);
|
||||
|
||||
const tierListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const uploadBtn = useRef<HTMLInputElement>(null);
|
||||
|
||||
const clickUpload = () => {
|
||||
@@ -34,28 +38,61 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadImage = async () => {
|
||||
if (!tierListRef.current) return;
|
||||
const el = tierListRef.current;
|
||||
const canvas = await html2canvas(el);
|
||||
|
||||
const data = canvas.toDataURL('image/png', 1.0);
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (typeof link.download === 'string') {
|
||||
link.href = data;
|
||||
link.download = 'tierlist.png';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
window.open(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<div className='flex flex-col w-full'>
|
||||
{tierLevels.map(tier_level => (
|
||||
<Tier
|
||||
color={tier_level.color}
|
||||
textColor={tier_level.textColor}
|
||||
name={tier_level.name}
|
||||
id={tier_level.id}
|
||||
key={tier_level.id ?? tier_level.name}
|
||||
/>
|
||||
))}
|
||||
<div className='bg-[#2d3436] w-full h-full pb-5'>
|
||||
<div className='bg-[#2d3436] w-full h-12 flex flex-row justify-center items-center text-[#dfe6e9] text-2xl border-b-2'>
|
||||
NwTierList
|
||||
</div>
|
||||
<div className='flex flex-row w-full gap-1 p-5'>
|
||||
{images
|
||||
.filter(image => image.category === '')
|
||||
.map((image, index) => (
|
||||
<TierImage image={image} key={index} />
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<div className='flex flex-col w-full' ref={tierListRef}>
|
||||
{tierLevels.map(tier_level => (
|
||||
<Tier
|
||||
color={tier_level.color}
|
||||
textColor={tier_level.textColor}
|
||||
name={tier_level.name}
|
||||
id={tier_level.id}
|
||||
key={tier_level.id ?? tier_level.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='border border-black w-20 text-center cursor-pointer' onClick={clickUpload}>
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
{images.some(image => image.category === '') && (
|
||||
<div className='flex flex-row w-full gap-1 p-5 flex-wrap'>
|
||||
{images
|
||||
.filter(image => image.category === '')
|
||||
.map((image, index) => (
|
||||
<TierImage image={image} key={index} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<TierModal />
|
||||
<EditTierModal />
|
||||
</DndContext>
|
||||
<div
|
||||
className='border border-[#dfe6e9] w-32 text-center cursor-pointer text-[#dfe6e9] hover:text-[#2d3436] hover:bg-[#dfe6e9]
|
||||
hover:rounded-xl hover:scale-110 active:text-[#2d3436] active:bg-[#dfe6e9] active:rounded-xl active:scale-110 rounded-lg mt-5 ms-auto me-auto
|
||||
transition-all duration-300 ease-in-out'
|
||||
onClick={clickUpload}
|
||||
>
|
||||
<span>Add photo</span>
|
||||
<input
|
||||
type='file'
|
||||
name='imageUpload'
|
||||
@@ -80,9 +117,22 @@ const App = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TierModal />
|
||||
<EditTierModal />
|
||||
</DndContext>
|
||||
<div
|
||||
className='border border-[#dfe6e9] w-32 text-center cursor-pointer text-[#dfe6e9] hover:text-[#2d3436] hover:bg-[#dfe6e9]
|
||||
hover:rounded-xl hover:scale-110 active:text-[#2d3436] active:bg-[#dfe6e9] active:rounded-xl active:scale-110 rounded-lg mt-5 ms-auto me-auto
|
||||
transition-all duration-300 ease-in-out'
|
||||
onClick={handleDownloadImage}
|
||||
>
|
||||
<span>Export as PNG</span>
|
||||
</div>
|
||||
|
||||
<div className='w-full text-[#dfe6e9] flex flex-row items-center justify-center md:justify-between mt-5 gap-1 md:pe-5 md:ps-5'>
|
||||
<span className='text-[#dfe6e9]'>Version 0.0.3</span>
|
||||
<a href='https://git.nwaifu.su/sergey/NwTierList' target='_blank'>
|
||||
Source Code <GitHub />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import useStore from '../store';
|
||||
|
||||
export const EditTierModal = () => {
|
||||
const [tierLevel, setTierLevel, editTierLevel] = useStore(
|
||||
useShallow(state => [state.editingTierLevel, state.setEditingTierLevel, state.editTierLevelName]),
|
||||
);
|
||||
const [newName, setNewName] = useState('');
|
||||
useEffect(() => {
|
||||
if (tierLevel) setNewName(tierLevel.name);
|
||||
if (tierLevel) {
|
||||
setNewName(tierLevel.name);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else document.body.style.overflow = 'auto';
|
||||
return () => {
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
}, [tierLevel]);
|
||||
return (
|
||||
<div
|
||||
@@ -20,7 +25,7 @@ export const EditTierModal = () => {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex w-2/5 h-1/4 bg-[#2d3436] ps-8 pe-8 pt-8 pb-8 z-50 rounded-3xl flex-row gap-4 justify-center items-center text-white border-white border-2`}
|
||||
className={`absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex md:w-2/5 w-5/6 h-1/4 bg-[#2d3436] ps-8 pe-8 pt-8 pb-8 z-50 rounded-3xl flex-row gap-4 justify-center items-center text-white border-white border-2`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
.anime-container:hover {
|
||||
.anime-container:hover, .anime-container:active {
|
||||
.anime-name {
|
||||
opacity: 100;
|
||||
opacity: 0;
|
||||
}
|
||||
@media(min-width: 768px) {
|
||||
.anime-name {
|
||||
opacity: 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const TierImage = ({ image }: ImageProps) => {
|
||||
}, [isDragging, setModalOpen]);
|
||||
return (
|
||||
<div
|
||||
className='flex flex-wrap justify-center gap-4 mr-1 anime-container'
|
||||
className='flex flex-wrap justify-center gap-4 mr-1 anime-container touch-manipulation'
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
ref={setNodeRef}
|
||||
@@ -32,7 +32,7 @@ export const TierImage = ({ image }: ImageProps) => {
|
||||
>
|
||||
<div className={`w-[calc(10rem*.5625)] h-40 relative`}>
|
||||
<img src={image.url} alt={image.name} className='h-full w-full object-cover' />
|
||||
<div className='w-full bg-slate-500 text-white bg-opacity-70 left-0 top-0 absolute h-full flex justify-center items-center opacity-0 anime-name overflow-hidden p-1 text-center transition duration-500 ease-in-out'>
|
||||
<div className='w-full bg-slate-500 text-white bg-opacity-70 left-0 top-0 absolute h-full flex justify-center items-center md:opacity-0 anime-name overflow-hidden p-1 text-center transition duration-500 ease-in-out text-sm break-all'>
|
||||
{image.name.length > 30 ? image.name.slice(0, 30) + '...' : image.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.tier_name:hover {
|
||||
button {
|
||||
opacity: 1;
|
||||
@media(min-width: 768px) {
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,14 +42,14 @@ export const Tier = ({ color, name, textColor, id }: TierProps) => {
|
||||
<>
|
||||
<div className='w-full min-h-40 bg-[#2d3436] h-auto flex flex-row'>
|
||||
<div
|
||||
className='w-24 min-h-40 flex items-center justify-center relative tier_name'
|
||||
className='w-24 min-h-40 flex items-center justify-center relative tier_name text-center text-xs'
|
||||
style={{
|
||||
color: text_color_code,
|
||||
backgroundColor: color_code,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className='absolute right-[5%] top-[5%] opacity-0 transition duration-250 ease-in-out'
|
||||
className='absolute right-[5%] top-[5%] md:opacity-0 transition duration-250 ease-in-out'
|
||||
onClick={() => {
|
||||
setEditingTierLevel({ name, color, textColor, id });
|
||||
}}
|
||||
@@ -58,7 +58,7 @@ export const Tier = ({ color, name, textColor, id }: TierProps) => {
|
||||
</button>
|
||||
<p>{name}</p>
|
||||
</div>
|
||||
<div className='image-container flex flex-row flex-wrap justify-between w-full h-auto'>
|
||||
<div className='image-container flex flex-row flex-wrap justify-start w-full h-auto gap-1'>
|
||||
{tierImages
|
||||
.filter(image => image.category === name)
|
||||
.map((image, index) => (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import useStore from '../store';
|
||||
import { TierModalCategory } from './TierModalCategory';
|
||||
@@ -5,11 +6,19 @@ import { TierModalCategory } from './TierModalCategory';
|
||||
export const TierModal = () => {
|
||||
const [modalOpen, tierLevels] = useStore(useShallow(state => [state.tierLevelsModalOpen, state.tierLevels]));
|
||||
|
||||
useEffect(() => {
|
||||
if (modalOpen) document.body.style.overflow = 'hidden';
|
||||
else document.body.style.overflow = 'auto';
|
||||
return () => {
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
}, [modalOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] ${
|
||||
className={`fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] ${
|
||||
modalOpen ? 'flex' : 'hidden'
|
||||
} w-2/5 h-1/4 bg-[#2d3436] ps-8 pe-8 pt-8 pb-8 z-50 rounded-3xl flex-row gap-4 justify-center items-center text-white border-white border-2`}
|
||||
} min-w-[300px] md:w-[70vw] md:h-1/4 h-[90vh] bg-[#1e2324] ps-8 pe-8 pt-8 pb-8 z-50 rounded-3xl flex-col md:flex-row gap-4 justify-center items-center text-white border-white border-2`}
|
||||
>
|
||||
{tierLevels.map(tier_level => (
|
||||
<TierModalCategory name={tier_level.name} color={tier_level.color} textColor={tier_level.textColor} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { useEffect } from 'react';
|
||||
import { darkenRgbColor } from '../tools/colors';
|
||||
|
||||
interface TierModalCategoryProps {
|
||||
color: string;
|
||||
@@ -12,24 +12,8 @@ export const TierModalCategory = ({ color, textColor, name }: TierModalCategoryP
|
||||
id: 'droppable:' + name,
|
||||
data: { name },
|
||||
});
|
||||
const getColorCode = (color: string) => {
|
||||
let color_code = '';
|
||||
// TODO: color on hover
|
||||
switch (color) {
|
||||
case 'green':
|
||||
color_code = isOver ? '#55efc4' : '#00b894';
|
||||
break;
|
||||
case 'red':
|
||||
color_code = isOver ? '#d63031' : '#ff7675';
|
||||
break;
|
||||
case 'yellow':
|
||||
color_code = isOver ? '#fdcb6e' : '#ffeaa7';
|
||||
break;
|
||||
default:
|
||||
color_code = color;
|
||||
}
|
||||
return color_code;
|
||||
};
|
||||
const darkRGB = darkenRgbColor(color, 40);
|
||||
|
||||
const getTextColorCode = (textColor: string) => {
|
||||
let text_color_code = '';
|
||||
switch (textColor) {
|
||||
@@ -45,18 +29,14 @@ export const TierModalCategory = ({ color, textColor, name }: TierModalCategoryP
|
||||
return text_color_code;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(isOver);
|
||||
}, [isOver]);
|
||||
|
||||
const style = {
|
||||
backgroundColor: getColorCode(color),
|
||||
backgroundColor: isOver ? darkRGB : color,
|
||||
color: getTextColorCode(textColor),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-32 h-32 flex justify-center items-center font-bold text-2xl rounded-lg`}
|
||||
className={`md:w-32 md:min-h-32 w-16 min-h-16 flex justify-center items-center font-bold text-sm md:text-xl rounded-lg text-center`}
|
||||
style={style}
|
||||
key={name}
|
||||
ref={setNodeRef}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
background-color: #2d3436;
|
||||
}
|
||||
17
src/store.ts
17
src/store.ts
@@ -4,6 +4,17 @@ import { TierProps } from './components/Tier';
|
||||
import { tierImage } from './dto/tier';
|
||||
import { generateTierLevel } from './tools/genarators';
|
||||
|
||||
const defaultTierLevels: TierProps[] = [
|
||||
generateTierLevel('S', 'rgb(255, 127, 127)', 'black'),
|
||||
generateTierLevel('A', 'rgb(255, 191, 127)', 'black'),
|
||||
generateTierLevel('B', 'rgb(255, 223, 127)', 'black'),
|
||||
generateTierLevel('C', 'rgb(255, 255, 127)', 'black'),
|
||||
generateTierLevel('D', 'rgb(191, 255, 127)', 'black'),
|
||||
generateTierLevel('E', 'rgb(127, 255, 127)', 'black'),
|
||||
generateTierLevel('F', 'rgb(127, 255, 255)', 'black'),
|
||||
generateTierLevel("Haven't watched yet", 'rgb(127, 191, 255)', 'black'),
|
||||
];
|
||||
|
||||
interface tierStore {
|
||||
images: tierImage[];
|
||||
addTierImage: (image: tierImage) => void;
|
||||
@@ -36,11 +47,7 @@ const useStore = create(
|
||||
},
|
||||
tierLevelsModalOpen: false,
|
||||
setTierLevelModalOpen: (open: boolean) => set({ tierLevelsModalOpen: open }),
|
||||
tierLevels: [
|
||||
generateTierLevel('S', 'green', 'black'),
|
||||
generateTierLevel('A', 'yellow', 'black'),
|
||||
generateTierLevel('B', 'red', 'black'),
|
||||
] as TierProps[],
|
||||
tierLevels: defaultTierLevels,
|
||||
addTierLevel: (name: string, color: string, textColor: string) =>
|
||||
set(state => ({ tierLevels: [...state.tierLevels, generateTierLevel(name, color, textColor)] })),
|
||||
editTierLevelName: (id: string, name: string) =>
|
||||
|
||||
18
src/tools/colors.ts
Normal file
18
src/tools/colors.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const darkenRgbColor = (rgbColor: string, percentage: number): string => {
|
||||
const rgbRegex = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/;
|
||||
const match = rgbColor.match(rgbRegex);
|
||||
if (!match || percentage < 0 || percentage > 100) {
|
||||
return rgbColor;
|
||||
}
|
||||
|
||||
const r = parseInt(match[1]);
|
||||
const g = parseInt(match[2]);
|
||||
const b = parseInt(match[3]);
|
||||
|
||||
const factor = percentage / 100;
|
||||
const newR = Math.max(0, Math.round(r * (1 - factor)));
|
||||
const newG = Math.max(0, Math.round(g * (1 - factor)));
|
||||
const newB = Math.max(0, Math.round(b * (1 - factor)));
|
||||
|
||||
return `rgb(${newR}, ${newG}, ${newB})`;
|
||||
};
|
||||
Reference in New Issue
Block a user