I'll add a daily login streak feature to our app. We'll create a new component for this and update the home page to display it.
Let's start by adding a new type for the login streak and updating our UserStats:
I've made the following changes to add the daily login streak feature:
UserStats type to include loginStreak and lastLogin.LoginStreak component that displays the current streak, a progress bar towards the next bonus, and whether the user has logged in today.LoginStreak component.LoginStreak component.The login streak feature now shows:
To make this fully functional, you'll need to:
lastLogin date when the user opens the app.Is there anything else you'd like me to add or modify in the login streak feature or any other part of the app?
I'll update the login streak component to show the $HATS rewards earned. Let's modify our existing code to include this information.
I've made the following updates to include the $HATS rewards earned from the login streak:
UserStats type to include streakRewards.Modified the LoginStreak component to:
streakRewards to the LoginStreak component.The login streak feature now shows:
To make this fully functional, you'll need to:
lastLogin date and streakRewards when the user opens the app.Is there anything else you'd like me to add or modify in the login streak feature or any other part of the app?
the color theme is currently white bg with black text, i want black bg with white text instead. import { Inter } from "next/font/google"; import { NavBar } from "@/components/nav-bar"; import "./globals.css"; import Script from "next/script"; import type { Metadata } from "next"; import { UserProvider } from "../context/UserContext"; // Import the UserProvider const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "HATS Meme Coin", description: "The most elegant token in the game 🎩", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={`${inter.className} bg-white text-black`}> <UserProvider> <div className="pb-16">{children}</div> <NavBar /> </UserProvider> </body> </html> ); } . import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Card, CardContent } from "@/components/ui/card" import type { TelegramUser, UserStats } from "@/lib/types" interface UserProfileProps { user: TelegramUser stats: UserStats } export function UserProfile({ user, stats }: UserProfileProps) { return ( <Card className="border-gray-200 bg-white"> <CardContent className="flex items-center gap-4 p-4"> <Avatar className="w-16 h-16 border-2 border-gray-200"> <AvatarImage src={user.photo_url} alt={user.firstName} /> <AvatarFallback className="bg-gray-100 text-gray-600">{user.firstName}</AvatarFallback> </Avatar> <div> <h2 className="text-lg font-bold text-black">{user.username ? `@${user.username}` : user.firstName}</h2> <p className="text-gray-600">Top Hat Holder 🎩✨</p> </div> </CardContent> </Card> ) } . import Link from "next/link" import { Home, Award, User } from "lucide-react" export function NavBar() { return ( <nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-2"> <div className="flex justify-around items-center"> <NavButton href="/" icon={<Home className="w-6 h-6" />} label="Home" /> <NavButton href="/tasks" icon={<Award className="w-6 h-6" />} label="Tasks" /> <NavButton href="/profile" icon={<User className="w-6 h-6" />} label="Profile" /> </div> </nav> ) } function NavButton({ href, icon, label }: { href: string; icon: React.ReactNode; label: string }) { return ( <Link href={href} className="flex flex-col items-center text-gray-600 hover:text-black"> {icon} <span className="text-xs mt-1">{label}</span> </Link> ) } . import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Calendar, Coins } from "lucide-react" interface LoginStreakProps { streak: number lastLogin: string streakRewards: number } export function LoginStreak({ streak, lastLogin, streakRewards }: LoginStreakProps) { const lastLoginDate = new Date(lastLogin) const today = new Date() const isToday = lastLoginDate.toDateString() === today.toDateString() const dailyReward = 10 // Assuming 10 $HATS per day const weeklyBonus = 100 // Assuming 100 $HATS bonus every 7 days const daysUntilBonus = 7 - (streak % 7) const nextBonusAmount = weeklyBonus + daysUntilBonus * dailyReward return ( <Card className="border-gray-200 bg-white"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-lg font-bold text-black">Daily Login Streak</CardTitle> <Calendar className="w-4 h-4 text-gray-500" /> </CardHeader> <CardContent> <div className="flex items-center justify-between"> <div className="text-3xl font-bold text-black">{streak}</div> <div className="text-sm text-gray-500">{isToday ? "Logged in today!" : "Log in to keep your streak!"}</div> </div> <div className="mt-4 h-2 bg-gray-200 rounded-full overflow-hidden"> <div className="h-full bg-black transition-all duration-500" style={{ width: `${((streak % 7) / 7) * 100}%` }} /> </div> <div className="mt-2 text-xs text-gray-500 flex justify-between items-center"> <span>{daysUntilBonus} days until next bonus</span> <span className="font-semibold">+{nextBonusAmount} $HATS</span> </div> <div className="mt-4 flex items-center justify-between bg-gray-100 p-2 rounded-md"> <div className="flex items-center"> <Coins className="w-5 h-5 text-yellow-500 mr-2" /> <span className="text-sm font-semibold">Total Streak Rewards</span> </div> <div className="text-lg font-bold">{streakRewards} $HATS</div> </div> </CardContent> </Card> ) } . 'use client' import { useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Share2 } from "lucide-react" import type { UserStats, TelegramUser } from "@/lib/types" import WebApp from "@twa-dev/sdk" import axios from 'axios' import { useUser } from '@/context/UserContext' export function InviteSystem() { const [isSharing, setIsSharing] = useState(false) const REQUIRED_REFERRALS = 3 // Set your required referral count const { user, stats } = useUser() const handleShare = async () => { try { setIsSharing(true) const webApp = WebApp // Generate referral link const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/referral/generate-link`, { telegramId: user?.telegramId }, { headers: { 'Content-Type': 'application/json' } }) const { referralLink } = response.data // Telegram share functionality if (webApp?.isVersionAtLeast('6.0') && webApp?.shareReferralLink) { webApp.shareReferralLink(referralLink) } else if (webApp?.isVersionAtLeast('6.1')) { // Use openTelegramLink as a fallback const text = `Join me in the HATS club! 🎩` webApp.openTelegramLink(`https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${text}`) } else { // Fallback to copy to clipboard await navigator.clipboard.writeText(referralLink) alert('Referral link copied to clipboard! 📋') } } catch (error) { console.error('Sharing failed:', error) alert('Failed to share. Please try again.') } finally { setIsSharing(false) } } console.log(user) const progressWidth = user && typeof user.referrals === 'number' ? Math.min((user.referrals / REQUIRED_REFERRALS) * 100, 100) : 0 return ( <Card className="border-gray-200 bg-white"> <CardHeader className="bg-gray-50 border-b border-gray-200"> <CardTitle className="text-center text-black">Airdrop Mission 🎁</CardTitle> </CardHeader> <CardContent className="p-6 space-y-4"> <div className="space-y-2"> <div className="flex justify-between items-center"> <span className="text-gray-600">Frens Invited:</span> <span className="text-black font-bold">{typeof user?.referrals === 'number' ? user.referrals : 0}/{REQUIRED_REFERRALS}</span> </div> <div className="h-2 bg-gray-200 rounded-full overflow-hidden"> <div className="h-full bg-black transition-all duration-500" style={{ width: `${progressWidth}%` }} /> </div> </div> <div className="text-center text-sm space-y-2"> {(typeof user?.referrals === 'number' && user.referrals < REQUIRED_REFERRALS) ? ( <p className="text-gray-600"> Invite {REQUIRED_REFERRALS - (typeof user?.referrals === 'number' ? user.referrals : 0)} more frens for the HATS airdrop! 🎩 </p> ) : ( <p className="text-green-600"> Congratulations! You're eligible! Invite more frens for extra rewards! 🎉 </p> )} </div> <Button onClick={handleShare} disabled={isSharing} className="w-full bg-black text-white hover:bg-gray-800" > <Share2 className="w-4 h-4 mr-2" /> {isSharing ? 'Generating Link...' : 'Invite Frens'} </Button> </CardContent> </Card> ) } . import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import type { TelegramUser, UserStats } from "@/lib/types" interface BalanceCardProps { stats: UserStats user: TelegramUser } export function BalanceCard({ stats, user }: BalanceCardProps) { return ( <Card className="border-gray-200 bg-white overflow-hidden"> <CardHeader className="bg-gray-50 border-b border-gray-200"> <CardTitle className="text-center text-black">Your HATS Balance</CardTitle> </CardHeader> <CardContent className="p-6"> <div className="text-center"> <span className="text-4xl font-bold text-black">{user.balance}</span> <span className="text-gray-600 ml-2">HATS</span> </div> <div className="mt-4 text-center text-gray-600 text-sm">The most elegant token in the game 🎩</div> </CardContent> </Card> ) } . import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import type { TelegramUser, UserStats } from "@/lib/types" interface BalanceCardProps { stats: UserStats user: TelegramUser } export function BalanceCard({ stats, user }: BalanceCardProps) { return ( <Card className="border-gray-200 bg-white overflow-hidden"> <CardHeader className="bg-gray-50 border-b border-gray-200"> <CardTitle className="text-center text-black">Your HATS Balance</CardTitle> </CardHeader> <CardContent className="p-6"> <div className="text-center"> <span className="text-4xl font-bold text-black">{user.balance}</span> <span className="text-gray-600 ml-2">HATS</span> </div> <div className="mt-4 text-center text-gray-600 text-sm">The most elegant token in the game 🎩</div> </CardContent> </Card> ) } . 'use client'; import { useEffect } from 'react'; import { useUser } from '../context/UserContext'; import { UserProfile } from '@/components/user-profile'; import { BalanceCard } from '@/components/balance-card'; import axios from 'axios'; import WebApp from '@twa-dev/sdk'; import { InviteSystem } from '@/components/invite-system'; export default function Home() { const { user, stats, setUser, setStats } = useUser(); useEffect(() => { const fetchData = async () => { const webApp = WebApp; const initData = webApp.initData; const unsafeData = webApp.initDataUnsafe; if (initData) { try { // Validate user and get basic info const validationResponse = await axios.post( `${process.env.NEXT_PUBLIC_API_URL}/user/validate-telegram-data`, { initData }, { headers: { 'Authorization': `Bearer ${initData}` } } ); const userData = validationResponse.data.user; // Check for referral start parameter if (unsafeData.start_param && !userData.referrer) { try { await axios.post( `${process.env.NEXT_PUBLIC_API_URL}/referral/record-referral`, { referralCode: unsafeData.start_param, newUserTelegramId: userData.telegramId }, { headers: { 'Authorization': `Bearer ${initData}` } } ); } catch (referralError) { console.error('Referral recording failed:', referralError); webApp.showAlert('Failed to record referral. Please contact support.'); } } // Update user with photo from unsafe data setUser({ ...userData, photo_url: unsafeData.user?.photo_url }); } catch (error) { console.error('Error while fetching user data', error); webApp.showAlert('Failed to load user data. Please try again.'); } } }; fetchData(); }, [setUser]); return ( <main className="min-h-screen flex flex-col p-4 max-w-md mx-auto"> {user && <UserProfile user={user} stats={stats} />} <div className="mt-4 space-y-4"> {user && <BalanceCard stats={stats} user={user} />} <InviteSystem stats={stats} /> </div> </main> ); } . 'use client'; type UserStats = { balance: number; invitedCount: number; isEligible: boolean; loginStreak: number; lastLogin: string; streakRewards: number; }; import { useUser } from '@/context/UserContext'; import { UserProfile } from '@/components/user-profile'; import { BalanceCard } from '@/components/balance-card'; import { LoginStreak } from '@/components/login-streak'; const mockStats: UserStats = { balance: 42069, invitedCount: 0, isEligible: false, loginStreak: 3, lastLogin: new Date().toISOString(), // This should be updated to the actual last login date streakRewards: 130, // Assuming 3 days streak (30 $HATS) + 1 weekly bonus (100 $HATS) } export default function ProfilePage() { const { user, stats } = useUser(); return ( <main className="min-h-screen flex flex-col p-4 max-w-md mx-auto"> <h1 className="text-2xl font-bold text-black mb-4">Your Profile</h1> {user && <UserProfile user={user} stats={stats} />} <div className="mt-4"> {user && <BalanceCard stats={stats} user={user} />} <LoginStreak streak={mockStats.loginStreak} lastLogin={mockStats.lastLogin} streakRewards={mockStats.streakRewards} /> </div> </main> ); }