Skip to content

Commit eca8bec

Browse files
committed
feat: added tool of the day
1 parent 7531790 commit eca8bec

File tree

5 files changed

+293
-0
lines changed

5 files changed

+293
-0
lines changed

src/App.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { Pagination } from './components/ui/pagination'
77
import { MasonryGrid } from './components/ui/masonry-grid'
88
import { Header } from './components/ui/header'
99
import { GitHubStats } from './components/ui/github-stats'
10+
import { FloatingActionButton } from './components/ui/floating-action-button'
11+
import { ToolOfTheDay } from './components/ui/tool-of-the-day'
12+
import { getToolOfTheDay } from './lib/utils'
1013

1114
// Define type for the tools
1215
interface Tool {
@@ -73,10 +76,32 @@ function App() {
7376
const [currentPage, setCurrentPage] = useState<number>(1);
7477
const [itemsPerPage] = useState<number>(15); // Show 9 tools per page (3x3 grid)
7578
const [heroVisible, setHeroVisible] = useState<boolean>(true);
79+
const [toolOfTheDayOpen, setToolOfTheDayOpen] = useState<boolean>(false);
80+
const [toolOfTheDay, setToolOfTheDay] = useState<Tool | null>(null);
7681
const heroRef = useRef<HTMLElement>(null);
7782

7883
// Store random colors for each tool to ensure consistent colors between renders
7984
const [cardColors, setCardColors] = useState<{[key: string]: string}>({});
85+
86+
// Function to select the Tool of the Day using the date-based algorithm
87+
const selectToolOfTheDay = () => {
88+
const todaysTool = getToolOfTheDay(tools);
89+
if (todaysTool) {
90+
setToolOfTheDay(todaysTool);
91+
setToolOfTheDayOpen(true);
92+
}
93+
};
94+
95+
// Use useEffect to initialize the Tool of the Day when the app loads
96+
useEffect(() => {
97+
if (tools.length > 0) {
98+
const todaysTool = getToolOfTheDay(tools);
99+
if (todaysTool) {
100+
setToolOfTheDay(todaysTool);
101+
// Don't automatically open the modal, just set the tool
102+
}
103+
}
104+
}, [tools]);
80105

81106
// Load data from data.json
82107
useEffect(() => {
@@ -420,6 +445,37 @@ function App() {
420445
</div>
421446

422447
<Footer />
448+
449+
{/* Tool of the Day FloatingActionButton */}
450+
<FloatingActionButton onClick={selectToolOfTheDay} />
451+
452+
{/* Tool of the Day Modal */}
453+
{toolOfTheDayOpen && toolOfTheDay && (
454+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
455+
<div className="bg-card w-full max-w-md rounded-lg shadow-lg animate-in fade-in zoom-in-95">
456+
<div className="p-6">
457+
<div className="flex justify-end">
458+
<button
459+
onClick={() => setToolOfTheDayOpen(false)}
460+
className="text-muted-foreground hover:text-foreground"
461+
>
462+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
463+
<line x1="18" y1="6" x2="6" y2="18"></line>
464+
<line x1="6" y1="6" x2="18" y2="18"></line>
465+
</svg>
466+
</button>
467+
</div>
468+
469+
<h2 className="text-2xl font-bold mb-4 text-center">Tool of the Day</h2>
470+
471+
<ToolOfTheDay
472+
tool={toolOfTheDay}
473+
color={cardColors[toolOfTheDay.name] || getRandomPastelColor()}
474+
/>
475+
</div>
476+
</div>
477+
</div>
478+
)}
423479
</div>
424480
);
425481
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// filepath: /Users/yogeshchoudhary/MyProjects/awesome-android-tooling/src/components/ui/floating-action-button.tsx
2+
import { useState, useEffect } from 'react';
3+
4+
interface FloatingActionButtonProps {
5+
onClick: () => void;
6+
visible?: boolean;
7+
}
8+
9+
export function FloatingActionButton({ onClick, visible = true }: FloatingActionButtonProps) {
10+
const [isVisible, setIsVisible] = useState(visible);
11+
const [isScrollingUp, setIsScrollingUp] = useState(false);
12+
const [lastScrollY, setLastScrollY] = useState(0);
13+
14+
useEffect(() => {
15+
const handleScroll = () => {
16+
const currentScrollY = window.scrollY;
17+
// Determine if scrolling up
18+
const isScrollUp = currentScrollY < lastScrollY;
19+
setIsScrollingUp(isScrollUp);
20+
21+
// Update the last scroll position
22+
setLastScrollY(currentScrollY);
23+
24+
// Show button when scrolling down and past the initial view
25+
setIsVisible(visible && (isScrollUp || currentScrollY < 200));
26+
};
27+
28+
window.addEventListener('scroll', handleScroll, { passive: true });
29+
return () => window.removeEventListener('scroll', handleScroll);
30+
}, [lastScrollY, visible]);
31+
32+
return (
33+
<button
34+
onClick={onClick}
35+
className={`fixed bottom-6 right-6 z-50 bg-primary text-primary-foreground rounded-full p-3 shadow-lg transition-all duration-300 flex items-center justify-center ${
36+
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
37+
} hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary/50`}
38+
aria-label="Tool of the Day"
39+
>
40+
<div className="flex items-center">
41+
<svg
42+
xmlns="http://www.w3.org/2000/svg"
43+
width="24"
44+
height="24"
45+
viewBox="0 0 24 24"
46+
fill="none"
47+
stroke="currentColor"
48+
strokeWidth="2"
49+
strokeLinecap="round"
50+
strokeLinejoin="round"
51+
className="mr-2"
52+
>
53+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
54+
</svg>
55+
<span className="font-medium">Tool of the Day</span>
56+
</div>
57+
</button>
58+
);
59+
}

src/components/ui/modal.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { ReactNode, useEffect } from 'react';
2+
3+
interface ModalProps {
4+
isOpen: boolean;
5+
onClose: () => void;
6+
children: ReactNode;
7+
title?: string;
8+
}
9+
10+
export function Modal({ isOpen, onClose, children, title }: ModalProps) {
11+
// Close modal when pressing Escape key
12+
useEffect(() => {
13+
const handleEsc = (event: KeyboardEvent) => {
14+
if (event.key === 'Escape') {
15+
onClose();
16+
}
17+
};
18+
19+
if (isOpen) {
20+
document.addEventListener('keydown', handleEsc);
21+
// Prevent scrolling of the background content
22+
document.body.style.overflow = 'hidden';
23+
}
24+
25+
return () => {
26+
document.removeEventListener('keydown', handleEsc);
27+
// Restore scrolling when modal is closed
28+
document.body.style.overflow = 'auto';
29+
};
30+
}, [isOpen, onClose]);
31+
32+
if (!isOpen) return null;
33+
34+
return (
35+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm animate-in fade-in">
36+
<div
37+
className="fixed inset-0 z-40 bg-background/80"
38+
onClick={onClose}
39+
aria-hidden="true"
40+
/>
41+
<div
42+
className="z-50 w-full max-w-xl max-h-[90vh] overflow-auto rounded-lg border bg-card shadow-lg animate-in zoom-in-90"
43+
role="dialog"
44+
aria-modal="true"
45+
>
46+
{title && (
47+
<div className="flex items-center justify-between p-4 border-b">
48+
<h2 className="text-xl font-semibold">{title}</h2>
49+
<button
50+
onClick={onClose}
51+
className="rounded-full p-1 hover:bg-muted"
52+
aria-label="Close"
53+
>
54+
<svg
55+
xmlns="http://www.w3.org/2000/svg"
56+
width="24"
57+
height="24"
58+
viewBox="0 0 24 24"
59+
fill="none"
60+
stroke="currentColor"
61+
strokeWidth="2"
62+
strokeLinecap="round"
63+
strokeLinejoin="round"
64+
>
65+
<line x1="18" y1="6" x2="6" y2="18"></line>
66+
<line x1="6" y1="6" x2="18" y2="18"></line>
67+
</svg>
68+
</button>
69+
</div>
70+
)}
71+
<div className="p-4">
72+
{children}
73+
</div>
74+
</div>
75+
</div>
76+
);
77+
}

src/components/ui/tool-of-the-day.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useEffect } from 'react';
2+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './card';
3+
import { GitHubStats } from './github-stats';
4+
import posthog from 'posthog-js';
5+
6+
interface Tool {
7+
name: string;
8+
description: string;
9+
link: string;
10+
tags: string[];
11+
author?: string;
12+
authorName?: string;
13+
authorLink?: string;
14+
}
15+
16+
interface ToolOfTheDayProps {
17+
tool: Tool | null;
18+
color: string;
19+
}
20+
21+
export function ToolOfTheDay({ tool, color }: ToolOfTheDayProps) {
22+
if (!tool) return null;
23+
24+
useEffect(()=> {
25+
posthog.capture('tool_of_the_day')
26+
}, [])
27+
28+
return (
29+
<div className="animate-fade-in">
30+
<Card
31+
className={`${color} transition-all hover:shadow-md border-2 border-primary/30`}
32+
onClick={() => window.open(tool.link, '_blank', 'noopener,noreferrer')}
33+
>
34+
<CardHeader>
35+
<div className="flex justify-between items-start">
36+
<CardTitle className="text-2xl">{tool.name}</CardTitle>
37+
<div className="px-3 py-1 bg-primary/20 rounded-full text-xs font-semibold text-primary">
38+
Featured
39+
</div>
40+
</div>
41+
<CardDescription className="text-base">{tool.description}</CardDescription>
42+
</CardHeader>
43+
<CardContent>
44+
<div className="flex flex-wrap gap-1 mt-1">
45+
{tool.tags.map(tag => (
46+
<span
47+
key={tag}
48+
className="px-2 py-0.5 rounded-full text-xs bg-background/80 text-foreground"
49+
>
50+
{tag}
51+
</span>
52+
))}
53+
</div>
54+
</CardContent>
55+
<CardFooter className="flex justify-between">
56+
{tool.authorName && (
57+
<span className="text-xs text-muted-foreground flex items-center">
58+
by {tool.authorName}
59+
</span>
60+
)}
61+
{/* Check if the tool link is a GitHub repo */}
62+
{tool.link.includes('github.com') && <GitHubStats url={tool.link} authorName={tool.authorName} authorLink={tool.authorLink} />}
63+
</CardFooter>
64+
</Card>
65+
</div>
66+
);
67+
}

src/lib/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,40 @@ export function formatRelativeTime(dateString: string | null): string {
139139
return `${years} ${years === 1 ? 'year' : 'years'} ago`;
140140
}
141141

142+
/**
143+
* Select a tool of the day based on the current date
144+
* This ensures the same tool is selected throughout the day
145+
* but changes each day in a pseudo-random way
146+
*/
147+
export function getToolOfTheDay<T>(tools: T[]): T | null {
148+
if (!tools || tools.length === 0) return null;
149+
150+
const today = new Date();
151+
// Use year, month, and day to create a consistent seed for the day
152+
const seed = today.getFullYear() * 10000 +
153+
(today.getMonth() + 1) * 100 +
154+
today.getDate();
155+
156+
// Simple seeded random number generator using a linear congruential generator algorithm
157+
// This will be consistent for the same seed (same day) but pseudo-random across different days
158+
function seededRandom(seed: number): number {
159+
// Parameters for the LCG algorithm (commonly used values)
160+
const a = 1664525;
161+
const c = 1013904223;
162+
const m = Math.pow(2, 32);
163+
164+
// Generate a random number between 0 and 1
165+
const randomNumber = ((a * seed + c) % m) / m;
166+
return randomNumber;
167+
}
168+
169+
// Use the seeded random function to get a consistent random index for today
170+
const randomValue = seededRandom(seed);
171+
const index = Math.floor(randomValue * tools.length);
172+
173+
return tools[index];
174+
}
175+
142176
// Merges and formats the class names
143177
export function cn(...inputs: ClassNameValue[]) {
144178
return twMerge(inputs)

0 commit comments

Comments
 (0)