mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-19 18:43:53 +08:00
Compare commits
2
Commits
aff50e2521
...
30b4791813
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30b4791813 | ||
|
|
9017780898 |
@@ -17,7 +17,8 @@
|
||||
"@remake/vitex": "workspace:*",
|
||||
"jotai": "^2.20.2",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hot-toast": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.18",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useInit, useWatcher } from '@/hooks/storage'
|
||||
import { useStep, Step } from '@remake/hooks'
|
||||
import ToastContainer from '@/toast'
|
||||
import Home from '@/containers/Home'
|
||||
import Mode from '@/containers/Mode'
|
||||
import Chara from '@/containers/Chara'
|
||||
@@ -40,6 +41,6 @@ export function Game() {
|
||||
const saving = useWatcher()
|
||||
useEffect(() => { if (!inited) init() }, [inited, init])
|
||||
if (!inited) return <Loading />
|
||||
return <><Container /><Saving active={saving} /></>
|
||||
return <><Container /><ToastContainer /><Saving active={saving} /></>
|
||||
}
|
||||
export default Game
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.achievement-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 calc(0.5rem * var(--ratio-font));
|
||||
height: calc(2rem * var(--ratio));
|
||||
gap: calc(1rem * var(--ratio-font));
|
||||
font-size: calc(1rem * var(--ratio-font));
|
||||
color: var(--grade-color);
|
||||
background: var(--background, var(--base-background-color));
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease-in-out;
|
||||
svg {
|
||||
fill: var(--grade-color);
|
||||
transition: fill 0.2s ease-in-out;
|
||||
stroke: none;
|
||||
width: calc(1.2rem * var(--ratio-font));
|
||||
height: calc(1.2rem * var(--ratio-font));
|
||||
}
|
||||
.name { font-weight: bold; }
|
||||
.description {
|
||||
font-size: calc(0.85rem * var(--ratio-font));
|
||||
user-select: none;
|
||||
&::before { content: '['; margin-right: 0.125rem; }
|
||||
&::after { content: ']'; margin-left: 0.125rem; }
|
||||
&::before, &::after { color: var(--grade-color); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { type Achievement as A, achievement } from '@remake/data'
|
||||
import './Achievement.css'
|
||||
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 6.3500002 6.3500002"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g>
|
||||
<path d="m 2.5032059,-0.0095735 c -0.043217,-6.35e-4 -0.0909,0.00531 -0.1493441,0.024289 C 2.2369795,0.0525379 2.1848433,0.1094109 2.1259683,0.1650936 2.0670958,0.2207129 2.0130256,0.2808305 1.9606037,0.3407933 1.8557572,0.4607694 1.7291409,0.5919178 1.7440793,0.5810887 1.7590282,0.5703466 1.5965582,0.6493864 1.4500399,0.7118302 1.3767847,0.7430018 1.3005529,0.7771219 1.2293826,0.8156998 1.1582044,0.8544035 1.0898255,0.885282 1.0175096,0.9846817 0.9451937,1.0840436 0.9358089,1.159797 0.9208732,1.2394466 0.9059507,1.319096 0.8985953,1.400403 0.8914171,1.479742 0.8770502,1.6384195 0.8500416,1.8161314 0.8557619,1.7985856 c 0.00572,-0.017558 -0.07988,0.1429176 -0.1617478,0.2795696 -0.040934,0.068351 -0.082447,0.141311 -0.1173056,0.214457 -0.034859,0.073146 -0.069638,0.1380463 -0.069763,0.2609662 -1.217e-4,0.1229198 0.03557,0.1882605 0.070279,0.2614827 0.034711,0.073247 0.07651,0.1434605 0.1173057,0.2118733 0.081592,0.1368131 0.1663991,0.2976496 0.1607158,0.2800863 -0.00569,-0.017526 0.019526,0.1632389 0.033589,0.3219442 0.00704,0.079296 0.014684,0.1606161 0.029456,0.2402954 0.014764,0.079742 0.022966,0.1557634 0.095083,0.2552817 0.072115,0.099531 0.1412954,0.130652 0.2123916,0.1694987 0.071099,0.038784 0.1469443,0.071021 0.2201413,0.1023191 0.1463886,0.062722 0.3099884,0.1442029 0.2950712,0.1333254 -0.014922,-0.010713 0.109855,0.1175901 0.2144581,0.2377115 0.0523,0.060092 0.1060847,0.1214847 0.1648486,0.1772502 0.058759,0.055816 0.1110827,0.1117997 0.2278909,0.1498616 0.1168056,0.03815 0.190463,0.023744 0.2707852,0.013435 0.080328,-0.010432 0.1605915,-0.026186 0.2382282,-0.043925 0.1552734,-0.035351 0.3341952,-0.068207 0.3157432,-0.068213 -0.018441,-1.85e-5 0.1595093,0.03148 0.3147086,0.06718 0.077602,0.017849 0.1568874,0.035993 0.2371963,0.046509 0.08029,0.010893 0.1544188,0.024387 0.2713012,-0.013436 C 4.1130209,5.0582357 4.1651597,5.0033666 4.2240321,4.9477472 4.2829046,4.8921276 4.3395598,4.8298801 4.3919791,4.7699803 4.4968281,4.6500042 4.6208595,4.521427 4.6059211,4.5322686 4.5909722,4.543008 4.7555086,4.4639712 4.9020269,4.4015274 4.9752821,4.3703558 5.0494475,4.3357186 5.1206178,4.2971408 5.1917933,4.258437 5.2601749,4.2280628 5.3324908,4.1286758 5.4048041,4.0293137 5.4141915,3.9535603 5.4291272,3.8739109 5.4440497,3.7942615 5.4514051,3.7129545 5.4585806,3.6336155 5.4729475,3.4749377 5.4999562,3.2946422 5.4942385,3.312188 5.4885235,3.3297458 5.5741162,3.1692831 5.6559863,3.0326187 5.69692,2.9642673 5.7384331,2.8938913 5.7732919,2.8207451 5.8081508,2.7475992 5.8429303,2.6827115 5.8430546,2.5597792 5.8431763,2.4368593 5.807484,2.3715183 5.7727734,2.2982965 5.7380653,2.2250492 5.6967824,2.152252 5.6559863,2.0838392 5.5743941,1.9470137 5.4890712,1.7882568 5.4947545,1.8058201 5.500443,1.8233461 5.4752282,1.6425809 5.4611656,1.4838759 5.4541542,1.404454 5.4464812,1.3232598 5.4317096,1.2435805 5.4169458,1.163838 5.4087437,1.0878171 5.3366262,0.9882988 5.2645114,0.8887679 5.1953308,0.8581762 5.1242346,0.8193169 5.0531358,0.7805332 4.977285,0.7458378 4.9040933,0.7144138 4.7577047,0.6516914 4.5941022,0.5728705 4.6090221,0.5836723 4.6239446,0.5943853 4.499683,0.4630319 4.39508,0.3428602 4.3427798,0.282768 4.2884767,0.2219049 4.2297154,0.1661267 4.1709567,0.1103108 4.1186327,0.0543271 4.0018218,0.0162652 3.8850188,-0.0218851 3.8113668,-0.0055365 3.7310393,0.004896 3.6507118,0.0153285 3.5704478,0.0315991 3.4928111,0.0493378 3.3375377,0.0846891 3.1586212,0.1174185 3.1770679,0.1175506 3.1955094,0.1175694 3.0175559,0.0834873 2.8623566,0.0477876 2.784757,0.0299388 2.7054693,0.0138614 2.6251629,0.0033458 2.5850177,-0.002102 2.5464229,-0.0089361 2.5032059,-0.0095735 Z m 0.6810957,1.3916462 a 0.26407552,0.26514563 1.664302 0 1 0.2098067,0.1069702 l 0.3064404,0.4149617 0.5167657,0.1953371 a 0.26407552,0.26514563 1.664302 0 1 0.1209225,0.403076 L 4.0395461,2.9240974 4.0142255,3.4780688 A 0.26407552,0.26514563 1.664302 0 1 3.6721272,3.7188812 L 3.1791343,3.563335 2.6468667,3.7090625 A 0.26407552,0.26514563 1.664302 0 1 2.3109702,3.4548146 L 2.3068427,2.9364998 2.0050537,2.4734789 A 0.26407552,0.26514563 1.664302 0 1 2.1404463,2.0760872 L 2.6324046,1.9133065 2.9781198,1.4807747 a 0.26407552,0.26514563 1.664302 0 1 0.1865524,-0.098185 0.26407552,0.26514563 1.664302 0 1 0.019632,-5.292e-4 z" />
|
||||
<path d="m 1.32395,4.6940153 v 0.8454266 c 0,0.22689 0.085796,0.4353634 0.2320264,0.5999633 0.073102,0.0823 0.1640284,0.155757 0.2831888,0.1953369 0.1191578,0.0396 0.2710842,0.033444 0.396875,-0.029456 L 3.1755175,5.8365814 4.1149947,6.3052865 a 0.26460996,0.26460996 0 0 0 0.1188561,0.027389 c 0.4351602,0 0.7932315,-0.3580833 0.7932315,-0.7932332 V 4.6976326 c -0.1063255,0.047962 -0.2399666,0.1090639 -0.2284096,0.1007693 0.01696,-0.0123 -0.1239096,0.1336209 -0.2428795,0.2697509 -0.059468,0.06796 -0.1238859,0.1384379 -0.1906852,0.2015381 -0.066799,0.0631 -0.1257724,0.1255652 -0.2583815,0.168465 -0.1326198,0.0429 -0.2168922,0.027386 -0.3079909,0.014986 -0.09112,-0.011999 -0.1806178,-0.03241 -0.2687188,-0.05271 -0.1760908,-0.0405 -0.3779838,-0.075984 -0.3570843,-0.075964 0.020929,0 -0.1824646,0.036898 -0.3586348,0.076998 -0.08809,0.0201 -0.1791679,0.038326 -0.2702666,0.050126 -0.09113,0.0117 -0.1744398,0.027797 -0.306959,-0.015502 C 2.1045317,5.3929098 2.0448735,5.3293743 1.9781747,5.2660743 1.9114944,5.2028043 1.8504047,5.133253 1.7911063,5.0650529 1.6724248,4.9287629 1.5313251,4.7831021 1.5482267,4.795302 1.5595777,4.803572 1.4276667,4.741909 1.32395,4.6940153 Z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
export interface AchievementProps {
|
||||
id: A['id']
|
||||
}
|
||||
export function Achievement({ id }: AchievementProps) {
|
||||
const ach = achievement.get(id)
|
||||
if (!ach) return null
|
||||
const grade = ach?.grade ?? 0
|
||||
return (
|
||||
<div className={`achievement-item grade-${grade}`}>
|
||||
<Icon />
|
||||
<span className="name">{ach.name}</span>
|
||||
<span className="description">{ach.description}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Achievement
|
||||
@@ -0,0 +1,10 @@
|
||||
.message-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
color: var(--base-text-color);
|
||||
background: var(--base-background-color);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import './Message.css'
|
||||
|
||||
export interface MsgToastProps {
|
||||
message: React.ReactNode
|
||||
}
|
||||
export function MsgToast({ message }: MsgToastProps) {
|
||||
return <div className="message-item">{message}</div>
|
||||
}
|
||||
|
||||
export default MsgToast
|
||||
@@ -2,6 +2,7 @@ import { useAchv, useGoHome } from '@remake/hooks'
|
||||
import { judge, judgeGrade, judgeGradeByValue } from '@/config'
|
||||
import { rates } from '@/display'
|
||||
import { achievements, type Achievement } from '@remake/data'
|
||||
import { toastMsg } from '@/toast'
|
||||
import './Achv.css'
|
||||
|
||||
interface AchievementProps {
|
||||
@@ -22,6 +23,7 @@ export function AchievementItem({ id, colled }: AchievementProps) {
|
||||
|
||||
export function Achv() {
|
||||
const goHome = useGoHome()
|
||||
const showRank = () => toastMsg('别卷了,没有排行榜', 'achv-rank')
|
||||
const [stats, sorted] = useAchv()
|
||||
const jtimes = judge('times', stats.times)
|
||||
const gtimes = judgeGrade('times', jtimes)
|
||||
@@ -34,7 +36,9 @@ export function Achv() {
|
||||
return (
|
||||
<div className="screen achv">
|
||||
<div className="controls">
|
||||
<button className="secondary">排行榜</button>
|
||||
<button className="secondary" onClick={showRank}>
|
||||
排行榜
|
||||
</button>
|
||||
<button className="primary" onClick={goHome}>
|
||||
返回
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { judgeGradeByValue } from '@/config'
|
||||
import { talents } from '@remake/data'
|
||||
import { toastAchvs, toastMsg } from '@/toast'
|
||||
import Replaced from '@/components/Replaced'
|
||||
import './Alloc.css'
|
||||
|
||||
@@ -65,12 +66,9 @@ export function Alloc() {
|
||||
const start = useStart()
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const handleNext = () => {
|
||||
if (left != 0) {
|
||||
// TODO: Show a warning that there are still points left
|
||||
return
|
||||
}
|
||||
if (left != 0) return toastMsg('还有剩余点数未分配', 'alloc-toast')
|
||||
const achievements = start()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.log('Achievements', achievements)
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { judgeGradeByValue } from '@/config'
|
||||
import { characters } from '@remake/data'
|
||||
import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { toastMsg } from '@/toast'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Chara.css'
|
||||
|
||||
@@ -89,6 +90,11 @@ export function Chara() {
|
||||
const [{ unique, characters }, puller] = useCharaPuller()
|
||||
const [picked, picker] = useCharaPicker()
|
||||
const submit = useCharaSubmit()
|
||||
const handleSubmit = () => {
|
||||
if (ready) return submit(picked)
|
||||
if (!picked) toastMsg('请先选择角色', 'chara-toast-pick')
|
||||
else toastMsg('请先生成唯一角色', 'chara-toast-unique')
|
||||
}
|
||||
const ready = picked && (picked.type === 'unique' ? !!u : true)
|
||||
return (
|
||||
<div className="screen chara">
|
||||
@@ -114,7 +120,7 @@ export function Chara() {
|
||||
</button>
|
||||
<button
|
||||
className={ready ? 'primary' : 'error'}
|
||||
onClick={ready ? () => submit(picked!) : undefined}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
开始新人生
|
||||
</button>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
width: auto;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
>.github-btn,
|
||||
>.theme-toggle-btn {
|
||||
>button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0.25rem;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
|
||||
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
|
||||
import { PullCount, dev } from '@/config'
|
||||
import { toastMsg } from '@/toast'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Pick.css'
|
||||
|
||||
@@ -9,7 +10,13 @@ export function Pick() {
|
||||
const [talents, picker] = useTalentPicker()
|
||||
const { enabled, min, max } = useSubmitIsEnable()
|
||||
const submit = useTalentSubmit()
|
||||
// useEffect(puller, [])
|
||||
const msg = enabled
|
||||
? '下一步'
|
||||
: `请选取 ${min == max ? min : `${min}~${max}`} 个天赋`
|
||||
const handleSubmit = () => {
|
||||
if (enabled) return submit()
|
||||
toastMsg(msg, 'pick-toast')
|
||||
}
|
||||
if (!pulled)
|
||||
return (
|
||||
<div className="screen talent-pick">
|
||||
@@ -33,15 +40,12 @@ export function Pick() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{enabled ? (
|
||||
<button className="primary" onClick={() => submit()}>
|
||||
下一步
|
||||
</button>
|
||||
) : (
|
||||
<button className="error">
|
||||
请选取 {min == max ? min : `${min}~${max}`} 个天赋
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={enabled ? 'primary' : 'error'}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{msg}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { achievements, events, talents } from '@remake/data'
|
||||
import { properties } from '@/display'
|
||||
import { AutoInterval } from '@/config'
|
||||
import { format } from '@remake/vitex'
|
||||
import { toastAchvs } from '@/toast/Achv'
|
||||
import './Play.css'
|
||||
|
||||
function LogTalent({ id }: { id: number }) {
|
||||
@@ -153,13 +154,13 @@ export function Play() {
|
||||
const handleNext = useCallback(() => {
|
||||
if (ended) return
|
||||
const achievements = next()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.debug('Achievements:', achievements)
|
||||
}, [ended, next])
|
||||
const handleGotoSummary = useCallback(() => {
|
||||
if (!ended) return
|
||||
const achievements = gotoSummary()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.debug('Achievements:', achievements)
|
||||
}, [ended, gotoSummary])
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
|
||||
import { usePicked, useEnd, useProfile, useIsClassic } from '@remake/hooks'
|
||||
import { useEndJudge } from '@/hooks/judge'
|
||||
import { properties, judgeDisplay } from '@/display'
|
||||
import { toastAchvs, toastMsg } from '@/toast'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Summary.css'
|
||||
|
||||
@@ -54,9 +55,13 @@ function TalentList({ picked, picker }: TalentListProps) {
|
||||
export default function Summary() {
|
||||
const isClassic = useIsClassic()
|
||||
const [locked, picker, end] = useEnd()
|
||||
const handlePicker = (id: number) => {
|
||||
if (isClassic) picker(id)
|
||||
else toastMsg('名人天赋不可锁定', 'summary-toast')
|
||||
}
|
||||
const handleEnd = () => {
|
||||
const achievements = end()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.log('Achievements', achievements)
|
||||
}
|
||||
return (
|
||||
@@ -68,7 +73,7 @@ export default function Summary() {
|
||||
? '你可以锁定一个天赋,下辈子还能抽到'
|
||||
: '名人天赋不可锁定'}
|
||||
</div>
|
||||
<TalentList picked={locked} picker={picker} />
|
||||
<TalentList picked={locked} picker={handlePicker} />
|
||||
</div>
|
||||
<button className="primary" onClick={handleEnd}>
|
||||
再次重开
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.achv-toast-container {
|
||||
position: fixed;
|
||||
width: min(calc(100dvw - 2rem), 48rem);
|
||||
top: 6rem;
|
||||
left: 1rem;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
.toast-item {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&.visible {
|
||||
animation: achv-slide-in 0.2s ease-out forwards;
|
||||
}
|
||||
&.hidden {
|
||||
animation: achv-fade-out 0.2s ease-out forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes achv-slide-in {
|
||||
0% { opacity: 0; transform: translateX(-100%); }
|
||||
100% { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes achv-slide-out {
|
||||
0% { opacity: 1; transform: translateX(0); }
|
||||
100% { opacity: 0; transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
@keyframes achv-fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes achv-fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useToaster, toast } from 'react-hot-toast/headless'
|
||||
import Achievement from '@/components/Achievement'
|
||||
import './Achv.css'
|
||||
|
||||
export function ToastAchvContainer() {
|
||||
const { toasts, handlers } = useToaster({ duration: 4000 }, 'achv-toast')
|
||||
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
||||
return (
|
||||
<div
|
||||
className="achv-toast-container"
|
||||
onMouseEnter={startPause}
|
||||
onMouseLeave={endPause}
|
||||
>
|
||||
{toasts.map(toast => {
|
||||
const offset = calculateOffset(toast, {
|
||||
reverseOrder: true,
|
||||
gutter: 8,
|
||||
})
|
||||
console.log('toast offset', toast.id, offset)
|
||||
return (
|
||||
<div
|
||||
className={`toast-item ${toast.visible ? 'visible' : 'hidden'}`}
|
||||
key={toast.id}
|
||||
ref={el => {
|
||||
if (el && typeof toast.height !== 'number') {
|
||||
const height = el.getBoundingClientRect().height
|
||||
console.log('toast height', toast.id, height)
|
||||
updateHeight(toast.id, height)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
top: `${offset}px`,
|
||||
}}
|
||||
>
|
||||
{typeof toast.message == 'function'
|
||||
? toast.message(toast)
|
||||
: toast.message}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToastAchvContainer
|
||||
|
||||
export function toastAchvs(achievements: number[]) {
|
||||
if (achievements.length === 0) return
|
||||
for (const id of achievements) {
|
||||
toast(<Achievement id={id} />, { id: '' + id, toasterId: 'achv-toast' })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
.msg-toast-container {
|
||||
position: fixed;
|
||||
width: min(calc(100dvw - 2rem), 48rem);
|
||||
top: 6rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
.toast-item {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&.visible {
|
||||
animation: msg-slide-in 0.2s ease-out forwards;
|
||||
}
|
||||
&.hidden {
|
||||
animation: msg-fade-out 0.2s ease-out forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes msg-slide-in {
|
||||
0% { opacity: 0; transform: translateY(-100%); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes msg-slide-out {
|
||||
0% { opacity: 1; transform: translateY(0); }
|
||||
100% { opacity: 0; transform: translateY(-100%); }
|
||||
}
|
||||
|
||||
@keyframes msg-fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes msg-fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useToaster, toast } from 'react-hot-toast/headless'
|
||||
import Message from '@/components/Message'
|
||||
import './Msg.css'
|
||||
|
||||
export function ToastMsgContainer() {
|
||||
const { toasts, handlers } = useToaster({ duration: 1500 }, 'msg-toast')
|
||||
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
||||
return (
|
||||
<div
|
||||
className="msg-toast-container"
|
||||
onMouseEnter={startPause}
|
||||
onMouseLeave={endPause}
|
||||
>
|
||||
{toasts.map(toast => {
|
||||
const offset = calculateOffset(toast, {
|
||||
reverseOrder: true,
|
||||
gutter: 8,
|
||||
})
|
||||
console.log('toast offset', toast.id, offset)
|
||||
return (
|
||||
<div
|
||||
className={`toast-item ${toast.visible ? 'visible' : 'hidden'}`}
|
||||
key={toast.id}
|
||||
ref={el => {
|
||||
if (el && typeof toast.height !== 'number') {
|
||||
const height = el.getBoundingClientRect().height
|
||||
console.log('toast height', toast.id, height)
|
||||
updateHeight(toast.id, height)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
top: `${offset}px`,
|
||||
}}
|
||||
>
|
||||
{typeof toast.message == 'function'
|
||||
? toast.message(toast)
|
||||
: toast.message}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToastMsgContainer
|
||||
|
||||
export function toastMsg(message: string, key: string) {
|
||||
toast(<Message message={message} />, { id: key, toasterId: 'msg-toast' })
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from './Msg'
|
||||
export * from './Achv'
|
||||
import ToastAchvContainer from './Achv'
|
||||
import ToastMsgContainer from './Msg'
|
||||
|
||||
export const ToastContainer = () => (
|
||||
<>
|
||||
<ToastAchvContainer />
|
||||
<ToastMsgContainer />
|
||||
</>
|
||||
)
|
||||
|
||||
export default ToastContainer
|
||||
Generated
+26
@@ -56,6 +56,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.2.8
|
||||
version: 19.2.8(react@19.2.8)
|
||||
react-hot-toast:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^19.2.18
|
||||
@@ -1059,6 +1062,11 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
goober@2.1.19:
|
||||
resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==}
|
||||
peerDependencies:
|
||||
csstype: ^3.0.10
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1503,6 +1511,13 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.2.8
|
||||
|
||||
react-hot-toast@2.6.0:
|
||||
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>=16'
|
||||
react-dom: '>=16'
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -2868,6 +2883,10 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.2.0
|
||||
|
||||
goober@2.1.19(csstype@3.2.3):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
happy-dom@20.11.2:
|
||||
@@ -3280,6 +3299,13 @@ snapshots:
|
||||
react: 19.2.8
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-hot-toast@2.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
goober: 2.1.19(csstype@3.2.3)
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
Reference in New Issue
Block a user