Compare commits
13 Commits
a74b499d83
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e829b6ff5 | |||
| 6cda6fee45 | |||
| 0157a63201 | |||
| 1ee4043c0e | |||
| 2fd088019e | |||
| 2c44abc320 | |||
| ff5dc072a0 | |||
| bfc9b9a367 | |||
| ae7c821562 | |||
| c30855beda | |||
| 1cfdb5ee08 | |||
| 3260e0403e | |||
| f35bb1d8dd |
11
index.html
11
index.html
@@ -1,10 +1,15 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "nwtierlist",
|
"name": "nwtierlist",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,10 +11,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@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": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-redux": "^9.2.0"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -27,6 +31,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
|
"sass-embedded": "^1.83.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"typescript-eslint": "^8.18.2",
|
"typescript-eslint": "^8.18.2",
|
||||||
|
|||||||
135
src/App.tsx
135
src/App.tsx
@@ -1,40 +1,22 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { DndContext, DragEndEvent } from '@dnd-kit/core';
|
||||||
import './App.css';
|
import { GitHub } from '@mui/icons-material';
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { useShallow } from 'zustand/shallow';
|
||||||
|
import './App.scss';
|
||||||
|
import { EditTierModal } from './components/EditTierModal';
|
||||||
import { TierImage } from './components/Image';
|
import { TierImage } from './components/Image';
|
||||||
import { Tier, TierProps } from './components/Tier';
|
import { Tier } from './components/Tier';
|
||||||
import { useAppDispatch, useAppSelector } from './hooks';
|
import { TierModal } from './components/TierModal';
|
||||||
import { addTierImage } from './store';
|
import { tierImage } from './dto/tier';
|
||||||
|
import useStore from './store';
|
||||||
|
|
||||||
const default_tier_levels: TierProps[] = [
|
const App = () => {
|
||||||
{
|
const [images, addTierImage, tierLevels, editTierImage] = useStore(
|
||||||
name: 'S',
|
useShallow(state => [state.images, state.addTierImage, state.tierLevels, state.editTierImage]),
|
||||||
color: 'green',
|
);
|
||||||
textColor: 'black',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'A',
|
|
||||||
color: 'yellow',
|
|
||||||
textColor: 'black',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'B',
|
|
||||||
color: 'red',
|
|
||||||
textColor: 'black',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface tierImage {
|
const tierListRef = useRef<HTMLDivElement>(null);
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const images = useAppSelector(state => state.tierImages);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(images);
|
|
||||||
}, [images]);
|
|
||||||
|
|
||||||
const uploadBtn = useRef<HTMLInputElement>(null);
|
const uploadBtn = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -42,43 +24,81 @@ function App() {
|
|||||||
if (uploadBtn.current) uploadBtn.current.click();
|
if (uploadBtn.current) uploadBtn.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomCategoryName = () => {
|
|
||||||
const categoryNames = default_tier_levels.flatMap(category => category.name);
|
|
||||||
return categoryNames[Math.floor(Math.random() * categoryNames.length)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = (images: tierImage[]) => {
|
const handleAdd = (images: tierImage[]) => {
|
||||||
images.forEach(image => {
|
images.forEach(image => {
|
||||||
dispatch(addTierImage(image));
|
addTierImage(image);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (active.data.current && over && over.data.current) {
|
||||||
|
const image: tierImage = active.data.current.image;
|
||||||
|
editTierImage(image, over.data.current.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<div className='bg-[#2d3436] w-full h-full pb-5'>
|
||||||
<div className='flex flex-col w-full'>
|
<div className='bg-[#2d3436] w-full h-12 flex flex-row justify-center items-center text-[#dfe6e9] text-2xl border-b-2'>
|
||||||
{default_tier_levels.map(tier_level => (
|
NwTierList
|
||||||
|
</div>
|
||||||
|
<DndContext onDragEnd={handleDragEnd}>
|
||||||
|
<div className='flex flex-col w-full' ref={tierListRef}>
|
||||||
|
{tierLevels.map(tier_level => (
|
||||||
<Tier
|
<Tier
|
||||||
color={tier_level.color}
|
color={tier_level.color}
|
||||||
textColor={tier_level.textColor}
|
textColor={tier_level.textColor}
|
||||||
name={tier_level.name}
|
name={tier_level.name}
|
||||||
key={tier_level.name}
|
id={tier_level.id}
|
||||||
|
key={tier_level.id ?? tier_level.name}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row w-full'>
|
{images.some(image => image.category === '') && (
|
||||||
|
<div className='flex flex-row w-full gap-1 p-5 flex-wrap'>
|
||||||
{images
|
{images
|
||||||
.filter(image => image.category === '')
|
.filter(image => image.category === '')
|
||||||
.map((image, index) => (
|
.map((image, index) => (
|
||||||
<TierImage image={image} key={index} />
|
<TierImage image={image} key={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className='border border-black w-20 text-center cursor-pointer' onClick={clickUpload}>
|
)}
|
||||||
<span>Upload</span>
|
<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
|
<input
|
||||||
type='file'
|
type='file'
|
||||||
name='imageUpload'
|
name='imageUpload'
|
||||||
accept='.jpeg'
|
accept='.jpeg'
|
||||||
ref={uploadBtn}
|
ref={uploadBtn}
|
||||||
|
multiple
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
console.log(event.target.files);
|
console.log(event.target.files);
|
||||||
@@ -89,7 +109,7 @@ function App() {
|
|||||||
upload_images.push({
|
upload_images.push({
|
||||||
url: url,
|
url: url,
|
||||||
name: event.target.files[i].name.replace('.jpeg', ''),
|
name: event.target.files[i].name.replace('.jpeg', ''),
|
||||||
category: getRandomCategoryName(),
|
category: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleAdd(upload_images);
|
handleAdd(upload_images);
|
||||||
@@ -97,8 +117,23 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
57
src/components/EditTierModal.tsx
Normal file
57
src/components/EditTierModal.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else document.body.style.overflow = 'auto';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
};
|
||||||
|
}, [tierLevel]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`top-0 left-0 w-screen h-screen z-50 bg-black bg-opacity-50 ${tierLevel ? 'fixed' : 'hidden'}`}
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setTierLevel(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tierLevel && (
|
||||||
|
<div className='flex flex-col gap-4 justify-start items-center'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
defaultValue={tierLevel.name}
|
||||||
|
placeholder='Tier Name'
|
||||||
|
className='text-black'
|
||||||
|
onChange={e => setNewName(e.target.value)}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
editTierLevel(tierLevel.id as string, newName);
|
||||||
|
setTierLevel(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setTierLevel(null)}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
10
src/components/Image.scss
Normal file
10
src/components/Image.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.anime-container:hover, .anime-container:active {
|
||||||
|
.anime-name {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
@media(min-width: 768px) {
|
||||||
|
.anime-name {
|
||||||
|
opacity: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,39 @@
|
|||||||
import { tierImage } from '../store';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { tierImage } from '../dto/tier';
|
||||||
|
import useStore from '../store';
|
||||||
|
import './Image.scss';
|
||||||
interface ImageProps {
|
interface ImageProps {
|
||||||
image: tierImage;
|
image: tierImage;
|
||||||
onDragStart?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TierImage = ({ image, onDragStart }: ImageProps) => {
|
export const TierImage = ({ image }: ImageProps) => {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||||
|
id: `draggable:${image.name}`,
|
||||||
|
data: { image },
|
||||||
|
});
|
||||||
|
const setModalOpen = useStore(state => state.setTierLevelModalOpen);
|
||||||
|
const style = transform
|
||||||
|
? {
|
||||||
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||||
|
zIndex: 99999,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
useEffect(() => {
|
||||||
|
setModalOpen(isDragging);
|
||||||
|
}, [isDragging, setModalOpen]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='flex flex-wrap justify-center gap-4 mr-1'
|
className='flex flex-wrap justify-center gap-4 mr-1 anime-container touch-manipulation'
|
||||||
onDragStart={() => {
|
{...listeners}
|
||||||
// e.preventDefault();
|
{...attributes}
|
||||||
if (onDragStart) onDragStart();
|
ref={setNodeRef}
|
||||||
}}
|
style={style}
|
||||||
>
|
>
|
||||||
<div className={`w-[calc(20rem*.5625)] h-80 relative`}>
|
<div className={`w-[calc(10rem*.5625)] h-40 relative`}>
|
||||||
<img src={image.url} alt={image.name} className='h-full w-full object-cover' />
|
<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 text-center bottom-0 left-0 absolute'>
|
<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}
|
{image.name.length > 30 ? image.name.slice(0, 30) + '...' : image.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
src/components/Tier.scss
Normal file
7
src/components/Tier.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.tier_name:hover {
|
||||||
|
@media(min-width: 768px) {
|
||||||
|
button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useState } from 'react';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks';
|
import { useShallow } from 'zustand/shallow';
|
||||||
import { changeTierImage, tierImage } from '../store';
|
import useStore from '../store';
|
||||||
import { TierImage } from './Image';
|
import { TierImage } from './Image';
|
||||||
|
import './Tier.scss';
|
||||||
|
|
||||||
export interface TierProps {
|
export interface TierProps {
|
||||||
color: 'red' | 'yellow' | 'green';
|
color: string;
|
||||||
name: string;
|
name: string;
|
||||||
textColor: 'white' | 'black';
|
textColor: string;
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
export const Tier = ({ color, name, textColor }: TierProps) => {
|
export const Tier = ({ color, name, textColor, id }: TierProps) => {
|
||||||
let color_code = '';
|
let color_code = '';
|
||||||
let text_color_code = '';
|
let text_color_code = '';
|
||||||
const tierImages = useAppSelector(state => state.tierImages);
|
const [tierImages, setEditingTierLevel] = useStore(useShallow(state => [state.images, state.setEditingTierLevel]));
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case 'green':
|
case 'green':
|
||||||
color_code = '#00b894';
|
color_code = '#00b894';
|
||||||
@@ -24,46 +25,44 @@ export const Tier = ({ color, name, textColor }: TierProps) => {
|
|||||||
color_code = '#ffeaa7';
|
color_code = '#ffeaa7';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
color_code = '#6c5ce7';
|
color_code = color;
|
||||||
}
|
}
|
||||||
switch (textColor) {
|
switch (textColor) {
|
||||||
case 'white':
|
case 'white':
|
||||||
text_color_code = '#dfe6e9';
|
text_color_code = '#dfe6e9';
|
||||||
break;
|
break;
|
||||||
default:
|
case 'black':
|
||||||
text_color_code = '#2d3436';
|
text_color_code = '#2d3436';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text_color_code = textColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [currentDragImage, setCurrentDragImage] = useState<tierImage | null>(null);
|
|
||||||
|
|
||||||
const onDragStart = (image: tierImage) => {
|
|
||||||
setCurrentDragImage(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className='w-full min-h-40 bg-[#2d3436] h-auto flex flex-row'>
|
||||||
<div
|
<div
|
||||||
className='w-full min-h-64 bg-[#2d3436] h-auto flex flex-row'
|
className='w-24 min-h-40 flex items-center justify-center relative tier_name text-center text-xs'
|
||||||
onDragEnd={() => {
|
|
||||||
// e.preventDefault();
|
|
||||||
if (currentDragImage) dispatch(changeTierImage(currentDragImage));
|
|
||||||
setCurrentDragImage(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className='w-52 h-80 flex items-center justify-center'
|
|
||||||
style={{
|
style={{
|
||||||
color: text_color_code,
|
color: text_color_code,
|
||||||
backgroundColor: color_code,
|
backgroundColor: color_code,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
className='absolute right-[5%] top-[5%] md:opacity-0 transition duration-250 ease-in-out'
|
||||||
|
onClick={() => {
|
||||||
|
setEditingTierLevel({ name, color, textColor, id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</button>
|
||||||
<p>{name}</p>
|
<p>{name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='image-container flex flex-row gap-1'>
|
<div className='image-container flex flex-row flex-wrap justify-start w-full h-auto gap-1'>
|
||||||
{tierImages
|
{tierImages
|
||||||
.filter(image => image.category === name)
|
.filter(image => image.category === name)
|
||||||
.map((image, index) => (
|
.map((image, index) => (
|
||||||
<TierImage image={image} key={index} onDragStart={() => onDragStart(image)} />
|
<TierImage image={image} key={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
src/components/TierModal.tsx
Normal file
28
src/components/TierModal.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useShallow } from 'zustand/shallow';
|
||||||
|
import useStore from '../store';
|
||||||
|
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={`fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] ${
|
||||||
|
modalOpen ? 'flex' : 'hidden'
|
||||||
|
} 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} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
47
src/components/TierModalCategory.tsx
Normal file
47
src/components/TierModalCategory.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
|
import { darkenRgbColor } from '../tools/colors';
|
||||||
|
|
||||||
|
interface TierModalCategoryProps {
|
||||||
|
color: string;
|
||||||
|
textColor: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TierModalCategory = ({ color, textColor, name }: TierModalCategoryProps) => {
|
||||||
|
const { setNodeRef, isOver } = useDroppable({
|
||||||
|
id: 'droppable:' + name,
|
||||||
|
data: { name },
|
||||||
|
});
|
||||||
|
const darkRGB = darkenRgbColor(color, 40);
|
||||||
|
|
||||||
|
const getTextColorCode = (textColor: string) => {
|
||||||
|
let text_color_code = '';
|
||||||
|
switch (textColor) {
|
||||||
|
case 'white':
|
||||||
|
text_color_code = '#dfe6e9';
|
||||||
|
break;
|
||||||
|
case 'black':
|
||||||
|
text_color_code = '#2d3436';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text_color_code = textColor;
|
||||||
|
}
|
||||||
|
return text_color_code;
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
backgroundColor: isOver ? darkRGB : color,
|
||||||
|
color: getTextColorCode(textColor),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
6
src/dto/tier.ts
Normal file
6
src/dto/tier.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface tierImage {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
category: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { AppDispatch, RootState } from './store';
|
|
||||||
|
|
||||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
|
||||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
background-color: #2d3436;
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import './index.css';
|
import './index.scss';
|
||||||
import store from './store.ts';
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Provider store={store}>
|
|
||||||
<App />
|
<App />
|
||||||
</Provider>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
91
src/store.ts
91
src/store.ts
@@ -1,39 +1,62 @@
|
|||||||
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import { TierProps } from './components/Tier';
|
||||||
|
import { tierImage } from './dto/tier';
|
||||||
|
import { generateTierLevel } from './tools/genarators';
|
||||||
|
|
||||||
export interface tierImage {
|
const defaultTierLevels: TierProps[] = [
|
||||||
name: string;
|
generateTierLevel('S', 'rgb(255, 127, 127)', 'black'),
|
||||||
url: string;
|
generateTierLevel('A', 'rgb(255, 191, 127)', 'black'),
|
||||||
category: string;
|
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;
|
||||||
|
editTierImage: (image: tierImage, tier: string) => void;
|
||||||
|
removeTierImage: (image: tierImage) => void;
|
||||||
|
tierLevelsModalOpen: boolean;
|
||||||
|
setTierLevelModalOpen: (open: boolean) => void;
|
||||||
|
tierLevels: TierProps[];
|
||||||
|
addTierLevel: (name: string, color: string, textColor: string) => void;
|
||||||
|
editTierLevelName: (id: string, name: string) => void;
|
||||||
|
editingTierLevel: TierProps | null;
|
||||||
|
setEditingTierLevel: (level: TierProps | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierImages = createSlice({
|
const useStore = create(
|
||||||
name: 'images',
|
devtools<tierStore>(
|
||||||
initialState: [] as tierImage[],
|
set => ({
|
||||||
reducers: {
|
images: [] as tierImage[],
|
||||||
addTierImage: (state, action: PayloadAction<tierImage>) => {
|
addTierImage: (image: tierImage) => set(state => ({ images: [...state.images, image] })),
|
||||||
state.push(action.payload);
|
editTierImage: (image: tierImage, tier: string) =>
|
||||||
|
set(state => ({
|
||||||
|
images:
|
||||||
|
image.category === tier
|
||||||
|
? state.images
|
||||||
|
: state.images.map(i => (i.name === image.name ? { ...i, category: tier } : i)),
|
||||||
|
})),
|
||||||
|
removeTierImage: (image: tierImage) => {
|
||||||
|
URL.revokeObjectURL(image.url);
|
||||||
|
set(state => ({ images: state.images.filter(i => i.name !== image.name) }));
|
||||||
},
|
},
|
||||||
removeTierImage: (state, action: PayloadAction<string>) => {
|
tierLevelsModalOpen: false,
|
||||||
return state.filter(image => image.name !== action.payload);
|
setTierLevelModalOpen: (open: boolean) => set({ tierLevelsModalOpen: open }),
|
||||||
},
|
tierLevels: defaultTierLevels,
|
||||||
changeTierImage: (state, action: PayloadAction<tierImage>) => {
|
addTierLevel: (name: string, color: string, textColor: string) =>
|
||||||
return state.map(image => {
|
set(state => ({ tierLevels: [...state.tierLevels, generateTierLevel(name, color, textColor)] })),
|
||||||
if (image.name == action.payload.name) return action.payload;
|
editTierLevelName: (id: string, name: string) =>
|
||||||
return image;
|
set(state => ({ tierLevels: state.tierLevels.map(t => (t.id === id ? { ...t, name } : t)) })),
|
||||||
});
|
editingTierLevel: null,
|
||||||
},
|
setEditingTierLevel: (level: TierProps | null) => set({ editingTierLevel: level }),
|
||||||
},
|
}),
|
||||||
});
|
{ name: 'tierStore' },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const { addTierImage, removeTierImage, changeTierImage } = tierImages.actions;
|
export default useStore;
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
tierImages: tierImages.reducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
|
||||||
export type AppDispatch = typeof store.dispatch;
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
|
|||||||
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})`;
|
||||||
|
};
|
||||||
10
src/tools/genarators.ts
Normal file
10
src/tools/genarators.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TierProps } from '../components/Tier';
|
||||||
|
|
||||||
|
export const generateTierLevel = (name: string, color: string, textColor: string): TierProps => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
textColor,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { defineConfig } from 'vite'
|
import react from '@vitejs/plugin-react-swc';
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
define: {
|
||||||
|
'process.env': {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user