Skip to content

Commit 9445932

Browse files
committed
feat: better achv. and notifications.
1 parent d1838f5 commit 9445932

File tree

6 files changed

+236
-66
lines changed

6 files changed

+236
-66
lines changed

src/components/Toast.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect } from 'react';
22
import { motion } from 'framer-motion';
33
import { XIcon } from '@phosphor-icons/react';
4+
import { Link } from 'react-router-dom';
45

56
const Toast = ({
67
id,
@@ -10,6 +11,7 @@ const Toast = ({
1011
type,
1112
removeToast,
1213
icon,
14+
links,
1315
}) => {
1416
useEffect(() => {
1517
const timer = setTimeout(() => {
@@ -54,6 +56,56 @@ const Toast = ({
5456
className="mt-1 mb-1 min-w-max mr-5 border-red-200"
5557
/>
5658
<span className="text-sm text-stone-200">{message}</span>
59+
{links && links.length > 0 && (
60+
<div className="flex flex-wrap gap-2 mt-3 mr-5">
61+
{links.map((link, index) => {
62+
const buttonClass =
63+
'text-xs font-medium bg-white/20 hover:bg-white/30 text-white py-1.5 px-3 rounded transition-colors border border-white/10 shadow-sm';
64+
65+
if (link.to) {
66+
return (
67+
<Link
68+
key={index}
69+
to={link.to}
70+
className={buttonClass}
71+
onClick={() => removeToast(id)}
72+
>
73+
{link.label}
74+
</Link>
75+
);
76+
}
77+
if (link.href) {
78+
return (
79+
<a
80+
key={index}
81+
href={link.href}
82+
target="_blank"
83+
rel="noopener noreferrer"
84+
className={buttonClass}
85+
onClick={() => removeToast(id)}
86+
>
87+
{link.label}
88+
</a>
89+
);
90+
}
91+
if (link.onClick) {
92+
return (
93+
<button
94+
key={index}
95+
onClick={() => {
96+
link.onClick();
97+
removeToast(id);
98+
}}
99+
className={buttonClass}
100+
>
101+
{link.label}
102+
</button>
103+
)
104+
}
105+
return null;
106+
})}
107+
</div>
108+
)}
57109
</div>
58110
<button
59111
onClick={() => removeToast(id)}

src/components/Toast.test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import Toast from './Toast';
4+
import { BrowserRouter } from 'react-router-dom';
5+
6+
const renderToast = (props) => {
7+
return render(
8+
<BrowserRouter>
9+
<Toast {...props} />
10+
</BrowserRouter>
11+
);
12+
};
13+
14+
describe('Toast Component', () => {
15+
const mockRemoveToast = jest.fn();
16+
17+
afterEach(() => {
18+
jest.clearAllMocks();
19+
});
20+
21+
test('renders toast with message', () => {
22+
renderToast({
23+
id: 1,
24+
title: 'Test Title',
25+
message: 'Test Message',
26+
removeToast: mockRemoveToast,
27+
});
28+
29+
expect(screen.getByText('Test Title')).toBeInTheDocument();
30+
expect(screen.getByText('Test Message')).toBeInTheDocument();
31+
});
32+
33+
test('renders links as buttons', () => {
34+
const links = [
35+
{ label: 'Internal Link', to: '/internal' },
36+
{ label: 'External Link', href: 'https://example.com' },
37+
{ label: 'Action Button', onClick: jest.fn() },
38+
];
39+
40+
renderToast({
41+
id: 1,
42+
title: 'Link Test',
43+
message: 'Testing Links',
44+
links: links,
45+
removeToast: mockRemoveToast,
46+
});
47+
48+
expect(screen.getByText('Internal Link')).toBeInTheDocument();
49+
expect(screen.getByText('Internal Link').closest('a')).toHaveAttribute('href', '/internal');
50+
51+
expect(screen.getByText('External Link')).toBeInTheDocument();
52+
expect(screen.getByText('External Link').closest('a')).toHaveAttribute('href', 'https://example.com');
53+
54+
expect(screen.getByText('Action Button')).toBeInTheDocument();
55+
const actionButton = screen.getByText('Action Button');
56+
fireEvent.click(actionButton);
57+
expect(links[2].onClick).toHaveBeenCalled();
58+
expect(mockRemoveToast).toHaveBeenCalledWith(1);
59+
});
60+
61+
test('calls removeToast when close button is clicked', () => {
62+
renderToast({
63+
id: 1,
64+
title: 'Close Test',
65+
message: 'Testing Close',
66+
removeToast: mockRemoveToast
67+
});
68+
69+
const closeButton = screen.getByRole('button'); // The X button
70+
fireEvent.click(closeButton);
71+
expect(mockRemoveToast).toHaveBeenCalledWith(1);
72+
});
73+
});

src/context/AchievementContext.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ export const AchievementProvider = ({ children }) => {
1717
{},
1818
);
1919
const [readPosts, setReadPosts] = usePersistentState('read-posts', []);
20+
const [showAchievementToast, setShowAchievementToast] = usePersistentState('show-achievement-toasts', true);
2021
const { addToast } = useToast();
2122

23+
const toggleAchievementToast = () => {
24+
setShowAchievementToast((prev) => !prev);
25+
};
26+
2227
// Helper to unlock an achievement
2328
const unlockAchievement = (id) => {
2429
// Check if valid achievement ID
@@ -35,15 +40,20 @@ export const AchievementProvider = ({ children }) => {
3540
[id]: { unlocked: true, unlockedAt: now },
3641
}));
3742

38-
// Trigger Toast
39-
addToast({
40-
title: 'Achievement Unlocked!',
41-
message: achievement.title,
42-
duration: 4000,
43-
icon: <TrophyIcon weight="duotone"/>,
44-
type: 'gold'
45-
// You might want to add a specific type or icon here later for styling
46-
});
43+
if (showAchievementToast) {
44+
// Trigger Toast
45+
addToast({
46+
title: 'Achievement Unlocked!',
47+
message: achievement.title,
48+
duration: 4000,
49+
icon: <TrophyIcon weight="duotone" />,
50+
type: 'gold',
51+
links: [
52+
{ label: 'Settings', to: '/settings' },
53+
{ label: 'Trophy Room', to: '/achievements' }
54+
]
55+
});
56+
}
4757
};
4858

4959
const trackReadingProgress = (slug) => {
@@ -72,7 +82,7 @@ export const AchievementProvider = ({ children }) => {
7282

7383
return (
7484
<AchievementContext.Provider
75-
value={{ unlockedAchievements, unlockAchievement, trackReadingProgress }}
85+
value={{ unlockedAchievements, unlockAchievement, trackReadingProgress, showAchievementToast, toggleAchievementToast }}
7686
>
7787
{children}
7888
</AchievementContext.Provider>

src/context/ToastContext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const ToastProvider = ({ children }) => {
3636
duration={toast.duration}
3737
type={toast.type}
3838
icon={toast.icon}
39+
links={toast.links}
3940
removeToast={removeToast}
4041
/>
4142
))}

src/pages/AchievementsPage.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { Link } from 'react-router-dom';
3-
import { ArrowLeftIcon, Trophy, Lock } from '@phosphor-icons/react';
3+
import { ArrowLeftIcon, Trophy, Lock, Info, BellSlash } from '@phosphor-icons/react';
44
import useSeo from '../hooks/useSeo';
55
import { useAchievements } from '../context/AchievementContext';
66
import { ACHIEVEMENTS } from '../config/achievements';
@@ -20,7 +20,7 @@ const AchievementsPage = () => {
2020
twitterImage: 'https://fezcode.github.io/logo512.png',
2121
});
2222

23-
const { unlockedAchievements } = useAchievements();
23+
const { unlockedAchievements, showAchievementToast } = useAchievements();
2424

2525
// Calculate progress
2626
const unlockedCount = Object.keys(unlockedAchievements).filter(
@@ -62,6 +62,52 @@ const AchievementsPage = () => {
6262
style={{ width: `${progressPercentage}%` }}
6363
></div>
6464
</div>
65+
{/* Achievement Toast Status */}
66+
<div
67+
className={`mt-8 flex items-center gap-4 p-4 rounded-xl border backdrop-blur-sm transition-all duration-300 shadow-lg ${
68+
showAchievementToast
69+
? 'bg-emerald-900/20 border-emerald-500/30 text-emerald-100 shadow-emerald-900/10'
70+
: 'bg-rose-900/20 border-rose-500/30 text-rose-100 shadow-rose-900/10'
71+
}`}
72+
>
73+
<div
74+
className={`p-2.5 rounded-full shrink-0 ${
75+
showAchievementToast
76+
? 'bg-emerald-500/20 text-emerald-400'
77+
: 'bg-rose-500/20 text-rose-400'
78+
}`}
79+
>
80+
{showAchievementToast ? (
81+
<Info size={24} weight="duotone" />
82+
) : (
83+
<BellSlash size={24} weight="duotone" />
84+
)}
85+
</div>
86+
<div className="flex-1 text-left">
87+
<p className="font-medium text-sm tracking-wide">
88+
ACHIEVEMENT NOTIFICATIONS ARE{' '}
89+
<span className="font-bold">
90+
{showAchievementToast ? 'ACTIVE' : 'MUTED'}
91+
</span>
92+
</p>
93+
<p
94+
className={`text-xs mt-1 ${
95+
showAchievementToast
96+
? 'text-emerald-400/80'
97+
: 'text-rose-400/80'
98+
}`}
99+
>
100+
You can toggle these popups in the{' '}
101+
<Link
102+
to="/settings"
103+
className="underline underline-offset-2 hover:text-white transition-colors"
104+
>
105+
Settings
106+
</Link>
107+
.
108+
</p>
109+
</div>
110+
</div>
65111
</div>
66112
</div>
67113

0 commit comments

Comments
 (0)