diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | app/src/app/layout.tsx | 18 | ||||
-rw-r--r-- | app/src/app/page.tsx | 10 | ||||
-rw-r--r-- | app/src/app/track/page.tsx | 148 | ||||
-rw-r--r-- | app/src/components/navbar.tsx | 100 | ||||
-rw-r--r-- | app/src/components/theme-provider.tsx | 9 | ||||
-rw-r--r-- | app/src/components/ui/theme-toggle.tsx | 61 |
7 files changed, 337 insertions, 13 deletions
@@ -87,7 +87,3 @@ SOCKET_PORT=3001 - `/view/[userId]`: View a specific user's location - `/map`: Location sharing via email page - `/shared/[token]`: View a shared location - -## License - -MIT
\ No newline at end of file diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index d461f2d..97ebb2a 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/sonner"; +import { ThemeProvider } from "@/components/theme-provider"; +import Navbar from "@/components/navbar"; const inter = Inter({ subsets: ["latin"] }); @@ -17,9 +19,19 @@ export default function RootLayout({ }>) { return ( <html lang="en" suppressHydrationWarning={true}> - <body className={inter.className}> - {children} - <Toaster /> + <body className={`${inter.className} bg-background text-foreground`}> + <ThemeProvider + attribute="class" + defaultTheme="system" + enableSystem + disableTransitionOnChange + > + <div className="flex flex-col min-h-screen"> + <Navbar /> + <main className="flex-1 w-full">{children}</main> + </div> + <Toaster /> + </ThemeProvider> </body> </html> ); diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 736b472..beecc8f 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -4,8 +4,8 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } export default function Home() { return ( - <main className="flex min-h-screen flex-col items-center justify-center p-4 bg-slate-50"> - <Card className="w-full max-w-lg shadow-lg"> + <main className="flex flex-col items-center justify-center p-4 bg-background"> + <Card className="w-full max-w-lg"> <CardHeader> <CardTitle className="text-2xl text-center">Real-Time Location Tracking</CardTitle> <CardDescription className="text-center"> @@ -52,7 +52,7 @@ export default function Home() { <CardTitle className="text-lg">Real-Time Tracking</CardTitle> </CardHeader> <CardContent> - <p className="text-sm text-slate-600">Connect with others in real-time to see their current location.</p> + <p className="text-sm text-muted-foreground">Connect with others in real-time to see their current location.</p> </CardContent> </Card> <Card> @@ -60,13 +60,13 @@ export default function Home() { <CardTitle className="text-lg">Location Sharing</CardTitle> </CardHeader> <CardContent> - <p className="text-sm text-slate-600">Share your location via email links that can be viewed by anyone.</p> + <p className="text-sm text-muted-foreground">Share your location via email links that can be viewed by anyone.</p> </CardContent> </Card> </div> </div> - <footer className="mt-8 text-center text-sm text-slate-500"> + <footer className="mt-8 text-center text-sm text-muted-foreground"> © {new Date().getFullYear()} Real-Time Location Tracker </footer> </main> diff --git a/app/src/app/track/page.tsx b/app/src/app/track/page.tsx index 0519ecb..060d391 100644 --- a/app/src/app/track/page.tsx +++ b/app/src/app/track/page.tsx @@ -1 +1,147 @@ -
\ No newline at end of file +"use client"; + +import { useState, useEffect } from "react"; +import { useSocket } from "@/hooks/useSocket"; +import Map from "@/components/Map"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { toast } from "sonner"; + +export default function TrackPage() { + const [userId, setUserId] = useState<string>(""); + const [location, setLocation] = useState<{ + latitude: number; + longitude: number; + accuracy?: number; + } | null>(null); + const [isTracking, setIsTracking] = useState(false); + const { isConnected, socket } = useSocket(); + + // Generate a random user ID on component mount if not already set + useEffect(() => { + if (!userId) { + const newUserId = `user_${Math.random().toString(36).substring(2, 9)}`; + setUserId(newUserId); + } + }, [userId]); + + // Handle location updates from the map component + const handleLocationUpdate = (location: { + latitude: number; + longitude: number; + accuracy?: number; + timestamp: Date; + }) => { + setLocation({ + latitude: location.latitude, + longitude: location.longitude, + accuracy: location.accuracy, + }); + + // Only send location updates if we're tracking + if (isTracking && socket && isConnected) { + socket.emit('location:update', { + userId, + latitude: location.latitude, + longitude: location.longitude, + accuracy: location.accuracy, + timestamp: new Date().toISOString() + }); + } + }; + + // Toggle location tracking + const toggleTracking = () => { + if (!isTracking && socket) { + // Register user when starting tracking + socket.emit('user:register', userId); + } + + setIsTracking(!isTracking); + + if (!isTracking) { + toast.success("Location tracking started"); + } else { + toast.info("Location tracking stopped"); + } + }; + + return ( + <div className="container mx-auto py-8 space-y-8"> + <h1 className="text-3xl font-bold">Real-Time Location Tracking</h1> + + <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> + <div className="md:col-span-2"> + <div className="h-[500px] rounded-lg overflow-hidden border"> + <Map + onLocationUpdate={handleLocationUpdate} + mode="tracking" + /> + </div> + </div> + + <div className="space-y-4"> + <Card> + <CardHeader> + <CardTitle className="flex items-center"> + Tracking Controls + {isConnected && ( + <span className="ml-2 text-xs text-green-500 inline-flex items-center"> + <span className="h-2 w-2 bg-green-500 rounded-full mr-1"></span> + Connected + </span> + )} + </CardTitle> + </CardHeader> + <CardContent> + <div className="space-y-4"> + <div> + <p className="text-sm text-muted-foreground mb-2">Your user ID: {userId}</p> + <Button + onClick={toggleTracking} + className={isTracking ? "bg-red-500 hover:bg-red-600" : ""} + > + {isTracking ? "Stop Tracking" : "Start Tracking"} + </Button> + </div> + {location && ( + <div className="pt-4 border-t"> + <h3 className="font-medium mb-2">Current Location</h3> + <p className="text-sm">Latitude: {location.latitude.toFixed(6)}</p> + <p className="text-sm">Longitude: {location.longitude.toFixed(6)}</p> + {location.accuracy && ( + <p className="text-sm">Accuracy: {location.accuracy.toFixed(2)} meters</p> + )} + </div> + )} + </div> + </CardContent> + </Card> + + <Card> + <CardHeader> + <CardTitle>Status</CardTitle> + </CardHeader> + <CardContent> + <p className="text-sm mb-2"> + Socket Connection: {isConnected ? + <span className="text-green-500">Connected</span> : + <span className="text-red-500">Disconnected</span> + } + </p> + <p className="text-sm mb-2"> + Tracking Status: {isTracking ? + <span className="text-green-500">Active</span> : + <span className="text-yellow-500">Inactive</span> + } + </p> + <p className="text-sm"> + Location: {location ? 'Available' : 'Waiting...'} + </p> + </CardContent> + </Card> + </div> + </div> + </div> + ); +}
\ No newline at end of file diff --git a/app/src/components/navbar.tsx b/app/src/components/navbar.tsx new file mode 100644 index 0000000..37f1a5b --- /dev/null +++ b/app/src/components/navbar.tsx @@ -0,0 +1,100 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { MapPin, Menu, X } from "lucide-react"; +import { SimpleThemeToggle } from "@/components/ui/theme-toggle"; +import { Button } from "@/components/ui/button"; + +export default function Navbar() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + return ( + <nav className="border-b bg-background"> + <div className="container mx-auto px-4 py-3"> + <div className="flex items-center justify-between"> + {/* Logo and Title */} + <Link href="/" className="flex items-center space-x-2"> + <MapPin className="h-6 w-6 text-primary" /> + <span className="text-lg font-bold">Location Tracker</span> + </Link> + + {/* Desktop Navigation */} + <div className="hidden md:flex items-center space-x-6"> + <Link + href="/map" + className="text-foreground/70 hover:text-foreground transition-colors" + > + Map + </Link> + <Link + href="/track" + className="text-foreground/70 hover:text-foreground transition-colors" + > + Track + </Link> + <Link + href="/share" + className="text-foreground/70 hover:text-foreground transition-colors" + > + Share + </Link> + <SimpleThemeToggle /> + </div> + + {/* Mobile Navigation Toggle */} + <div className="flex items-center md:hidden"> + <div className="mr-2"> + <SimpleThemeToggle /> + </div> + <Button + variant="ghost" + size="icon" + onClick={toggleMenu} + > + {isMenuOpen ? ( + <X className="h-5 w-5" /> + ) : ( + <Menu className="h-5 w-5" /> + )} + <span className="sr-only">Toggle menu</span> + </Button> + </div> + </div> + + {/* Mobile Navigation Menu */} + {isMenuOpen && ( + <div className="mt-2 py-2 border-t md:hidden"> + <div className="flex flex-col space-y-2"> + <Link + href="/map" + className="px-2 py-2 rounded-md hover:bg-accent transition-colors" + onClick={() => setIsMenuOpen(false)} + > + Map + </Link> + <Link + href="/track" + className="px-2 py-2 rounded-md hover:bg-accent transition-colors" + onClick={() => setIsMenuOpen(false)} + > + Track + </Link> + <Link + href="/share" + className="px-2 py-2 rounded-md hover:bg-accent transition-colors" + onClick={() => setIsMenuOpen(false)} + > + Share + </Link> + </div> + </div> + )} + </div> + </nav> + ); +}
\ No newline at end of file diff --git a/app/src/components/theme-provider.tsx b/app/src/components/theme-provider.tsx new file mode 100644 index 0000000..9fced93 --- /dev/null +++ b/app/src/components/theme-provider.tsx @@ -0,0 +1,9 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { type ThemeProviderProps } from "next-themes/dist/types"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return <NextThemesProvider {...props}>{children}</NextThemesProvider>; +}
\ No newline at end of file diff --git a/app/src/components/ui/theme-toggle.tsx b/app/src/components/ui/theme-toggle.tsx new file mode 100644 index 0000000..fbf7448 --- /dev/null +++ b/app/src/components/ui/theme-toggle.tsx @@ -0,0 +1,61 @@ +"use client"; + +import * as React from "react"; +import { useTheme } from "next-themes"; +import { Moon, Sun } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ThemeToggle() { + const { setTheme, theme } = useTheme(); + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="icon" className="rounded-full"> + <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> + <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> + <span className="sr-only">Toggle theme</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={() => setTheme("light")}> + Light + </DropdownMenuItem> + <DropdownMenuItem onClick={() => setTheme("dark")}> + Dark + </DropdownMenuItem> + <DropdownMenuItem onClick={() => setTheme("system")}> + System + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ); +} + +export function SimpleThemeToggle() { + const { setTheme, theme } = useTheme(); + + const toggleTheme = () => { + setTheme(theme === "dark" ? "light" : "dark"); + }; + + return ( + <Button + variant="ghost" + size="icon" + onClick={toggleTheme} + className="rounded-full h-9 w-9 relative" + aria-label="Toggle theme" + > + <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> + <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> + </Button> + ); +}
\ No newline at end of file |