Skip to content

feat: added tool of the day #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { Pagination } from './components/ui/pagination'
import { MasonryGrid } from './components/ui/masonry-grid'
import { Header } from './components/ui/header'
import { GitHubStats } from './components/ui/github-stats'
import { FloatingActionButton } from './components/ui/floating-action-button'
import { ToolOfTheDay } from './components/ui/tool-of-the-day'
import { getToolOfTheDay } from './lib/utils'

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

// Store random colors for each tool to ensure consistent colors between renders
const [cardColors, setCardColors] = useState<{[key: string]: string}>({});

// Function to select the Tool of the Day using the date-based algorithm
const selectToolOfTheDay = () => {
const todaysTool = getToolOfTheDay(tools);
if (todaysTool) {
setToolOfTheDay(todaysTool);
setToolOfTheDayOpen(true);
}
};

// Use useEffect to initialize the Tool of the Day when the app loads
useEffect(() => {
if (tools.length > 0) {
const todaysTool = getToolOfTheDay(tools);
if (todaysTool) {
setToolOfTheDay(todaysTool);
// Don't automatically open the modal, just set the tool
}
}
}, [tools]);

// Load data from data.json
useEffect(() => {
Expand Down Expand Up @@ -420,6 +445,37 @@ function App() {
</div>

<Footer />

{/* Tool of the Day FloatingActionButton */}
<FloatingActionButton onClick={selectToolOfTheDay} />

{/* Tool of the Day Modal */}
{toolOfTheDayOpen && toolOfTheDay && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card w-full max-w-md rounded-lg shadow-lg animate-in fade-in zoom-in-95">
<div className="p-6">
<div className="flex justify-end">
<button
onClick={() => setToolOfTheDayOpen(false)}
className="text-muted-foreground hover:text-foreground"
>
<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">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>

<h2 className="text-2xl font-bold mb-4 text-center">Tool of the Day</h2>

<ToolOfTheDay
tool={toolOfTheDay}
color={cardColors[toolOfTheDay.name] || getRandomPastelColor()}
/>
</div>
</div>
</div>
)}
</div>
);
}
Expand Down
57 changes: 57 additions & 0 deletions src/components/ui/floating-action-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// filepath: /Users/yogeshchoudhary/MyProjects/awesome-android-tooling/src/components/ui/floating-action-button.tsx
import { useState, useEffect } from 'react';

interface FloatingActionButtonProps {
onClick: () => void;
visible?: boolean;
}

export function FloatingActionButton({ onClick, visible = true }: FloatingActionButtonProps) {
const [isVisible, setIsVisible] = useState(visible);
const [lastScrollY, setLastScrollY] = useState(0);

useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY;
// Determine if scrolling up
const isScrollUp = currentScrollY < lastScrollY;

// Update the last scroll position
setLastScrollY(currentScrollY);

// Show button when scrolling down and past the initial view
setIsVisible(visible && (isScrollUp || currentScrollY < 200));
};

window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [lastScrollY, visible]);

return (
<button
onClick={onClick}
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 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
} hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary/50`}
aria-label="Tool of the Day"
>
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<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" />
</svg>
<span className="font-medium">Tool of the Day</span>
</div>
</button>
);
}
77 changes: 77 additions & 0 deletions src/components/ui/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ReactNode, useEffect } from 'react';

interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
title?: string;
}

export function Modal({ isOpen, onClose, children, title }: ModalProps) {
// Close modal when pressing Escape key
useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};

if (isOpen) {
document.addEventListener('keydown', handleEsc);
// Prevent scrolling of the background content
document.body.style.overflow = 'hidden';
}

return () => {
document.removeEventListener('keydown', handleEsc);
// Restore scrolling when modal is closed
document.body.style.overflow = 'auto';
};
}, [isOpen, onClose]);

if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm animate-in fade-in">
<div
className="fixed inset-0 z-40 bg-background/80"
onClick={onClose}
aria-hidden="true"
/>
<div
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"
role="dialog"
aria-modal="true"
>
{title && (
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-xl font-semibold">{title}</h2>
<button
onClick={onClose}
className="rounded-full p-1 hover:bg-muted"
aria-label="Close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
)}
<div className="p-4">
{children}
</div>
</div>
</div>
);
}
67 changes: 67 additions & 0 deletions src/components/ui/tool-of-the-day.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './card';
import { GitHubStats } from './github-stats';
import posthog from 'posthog-js';

interface Tool {
name: string;
description: string;
link: string;
tags: string[];
author?: string;
authorName?: string;
authorLink?: string;
}

interface ToolOfTheDayProps {
tool: Tool | null;
color: string;
}

export function ToolOfTheDay({ tool, color }: ToolOfTheDayProps) {
if (!tool) return null;

useEffect(()=> {
posthog.capture('tool_of_the_day')
}, [])

return (
<div className="animate-fade-in">
<Card
className={`${color} transition-all hover:shadow-md border-2 border-primary/30`}
onClick={() => window.open(tool.link, '_blank', 'noopener,noreferrer')}
>
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="text-2xl">{tool.name}</CardTitle>
<div className="px-3 py-1 bg-primary/20 rounded-full text-xs font-semibold text-primary">
Featured
</div>
</div>
<CardDescription className="text-base">{tool.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-1 mt-1">
{tool.tags.map(tag => (
<span
key={tag}
className="px-2 py-0.5 rounded-full text-xs bg-background/80 text-foreground"
>
{tag}
</span>
))}
</div>
</CardContent>
<CardFooter className="flex justify-between">
{tool.authorName && (
<span className="text-xs text-muted-foreground flex items-center">
by {tool.authorName}
</span>
)}
{/* Check if the tool link is a GitHub repo */}
{tool.link.includes('github.com') && <GitHubStats url={tool.link} authorName={tool.authorName} authorLink={tool.authorLink} />}
</CardFooter>
</Card>
</div>
);
}
34 changes: 34 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,40 @@ export function formatRelativeTime(dateString: string | null): string {
return `${years} ${years === 1 ? 'year' : 'years'} ago`;
}

/**
* Select a tool of the day based on the current date
* This ensures the same tool is selected throughout the day
* but changes each day in a pseudo-random way
*/
export function getToolOfTheDay<T>(tools: T[]): T | null {
if (!tools || tools.length === 0) return null;

const today = new Date();
// Use year, month, and day to create a consistent seed for the day
const seed = today.getFullYear() * 10000 +
(today.getMonth() + 1) * 100 +
today.getDate();

// Simple seeded random number generator using a linear congruential generator algorithm
// This will be consistent for the same seed (same day) but pseudo-random across different days
function seededRandom(seed: number): number {
// Parameters for the LCG algorithm (commonly used values)
const a = 1664525;
const c = 1013904223;
const m = Math.pow(2, 32);

// Generate a random number between 0 and 1
const randomNumber = ((a * seed + c) % m) / m;
return randomNumber;
}

// Use the seeded random function to get a consistent random index for today
const randomValue = seededRandom(seed);
const index = Math.floor(randomValue * tools.length);

return tools[index];
}

// Merges and formats the class names
export function cn(...inputs: ClassNameValue[]) {
return twMerge(inputs)
Expand Down