first commit

This commit is contained in:
Steve Dogiakos 2025-02-06 08:41:43 -07:00
commit 57be693e0d
8 changed files with 473 additions and 0 deletions

27
app/actions.ts Normal file
View File

@ -0,0 +1,27 @@
"use server"
import { fetchVehicles, submitFillup } from "@/utils/api"
export async function getVehicles() {
try {
return await fetchVehicles()
} catch (error) {
console.error("Error fetching vehicles:", error)
throw new Error("Failed to fetch vehicles")
}
}
export async function submitGasFillup(data: {
name: string
vehicleId: string
mileage: number
gallons: number
}) {
try {
return await submitFillup(data)
} catch (error) {
console.error("Error submitting fillup:", error)
throw new Error("Failed to submit gas fillup")
}
}

56
app/globals.css Normal file
View File

@ -0,0 +1,56 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 300 20% 98%;
--foreground: 195 31% 13%;
--card: 300 20% 98%;
--card-foreground: 195 31% 13%;
--popover: 300 20% 98%;
--popover-foreground: 195 31% 13%;
--primary: 181 70% 46%;
--primary-foreground: 195 31% 13%;
--secondary: 191 89% 68%;
--secondary-foreground: 195 31% 13%;
--muted: 195 31% 13%;
--muted-foreground: 195 31% 45%;
--accent: 191 89% 68%;
--accent-foreground: 195 31% 13%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 195 31% 13%;
--input: 195 31% 13%;
--ring: 181 70% 46%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-vollkorn;
}
}

31
app/layout.tsx Normal file
View File

@ -0,0 +1,31 @@
import { Toaster } from "@/components/ui/toaster"
import { Vollkorn, Open_Sans } from "next/font/google"
import "./globals.css"
import type React from "react" // Import React
const vollkorn = Vollkorn({
subsets: ["latin"],
weight: ["700"],
variable: "--font-vollkorn",
})
const openSans = Open_Sans({
subsets: ["latin"],
variable: "--font-open-sans",
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={`${vollkorn.variable} ${openSans.variable} font-open-sans bg-background`}>
{children}
<Toaster />
</body>
</html>
)
}

12
app/page.tsx Normal file
View File

@ -0,0 +1,12 @@
"use client"
import GasFillupForm from "@/components/GasFillupForm"
export default function Page() {
return (
<main>
<GasFillupForm />
</main>
)
}

View File

@ -0,0 +1,171 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useToast } from "@/components/ui/use-toast"
import { getVehicles, submitGasFillup } from "@/app/actions"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { FuelIcon as GasPump } from "lucide-react"
interface Vehicle {
id: string
name: string
}
export default function GasFillupForm() {
const { toast } = useToast()
const [name, setName] = useState("")
const [vehicle, setVehicle] = useState("")
const [mileage, setMileage] = useState("")
const [fuelAmount, setFuelAmount] = useState("")
const [vehicles, setVehicles] = useState<Vehicle[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
async function loadVehicles() {
setIsLoading(true)
try {
const vehicleData = await getVehicles()
setVehicles(vehicleData)
} catch (error) {
toast({
title: "Error",
description: "Failed to load vehicles. Please try again.",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
loadVehicles()
}, [toast])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
await submitGasFillup({
name,
vehicleId: vehicle,
mileage: Number(mileage),
gallons: Number(fuelAmount),
})
toast({
title: "Success",
description: "Gas fillup has been recorded successfully.",
})
// Reset form
setName("")
setVehicle("")
setMileage("")
setFuelAmount("")
} catch (error) {
toast({
title: "Error",
description: "Failed to submit gas fillup. Please try again.",
variant: "destructive",
})
} finally {
setIsSubmitting(false)
}
}
return (
<div className="min-h-screen bg-background p-4 md:p-8">
<Card className="max-w-md mx-auto border-primary shadow-lg">
<CardHeader className="space-y-1 text-center bg-primary text-white rounded-t-lg">
<div className="flex justify-center mb-2">
<GasPump size={32} />
</div>
<CardTitle className="text-2xl font-vollkorn">Gas Fill-up Form</CardTitle>
</CardHeader>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name" className="text-mdc-dark">
Employee Name
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label htmlFor="vehicle" className="text-mdc-dark">
Vehicle
</Label>
<Select value={vehicle} onValueChange={setVehicle} disabled={isLoading || isSubmitting}>
<SelectTrigger id="vehicle" className="border-mdc-blue focus:ring-secondary">
<SelectValue placeholder={isLoading ? "Loading vehicles..." : "Select a vehicle"} />
</SelectTrigger>
<SelectContent>
{vehicles.map((v) => (
<SelectItem key={v.id} value={v.id}>
{v.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="mileage" className="text-mdc-dark">
Vehicle Mileage
</Label>
<Input
id="mileage"
type="number"
value={mileage}
onChange={(e) => setMileage(e.target.value)}
required
min="0"
step="1"
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label htmlFor="fuelAmount" className="text-mdc-dark">
Fuel Amount (Gallons)
</Label>
<Input
id="fuelAmount"
type="number"
value={fuelAmount}
onChange={(e) => setFuelAmount(e.target.value)}
required
min="0"
step="0.001"
className="border-mdc-blue focus:ring-secondary"
disabled={isSubmitting}
/>
</div>
<Button
type="submit"
className="w-full bg-primary hover:bg-secondary text-mdc-dark font-semibold transition-colors"
disabled={isSubmitting}
>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
</CardContent>
</Card>
</div>
)
}

23
components/ui/toaster.tsx Normal file
View File

@ -0,0 +1,23 @@
"use client"
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props} className="bg-white border-primary">
{title && <ToastTitle className="font-vollkorn">{title}</ToastTitle>}
{description && <ToastDescription className="font-open-sans">{description}</ToastDescription>}
{action}
<ToastClose />
</Toast>
))}
<ToastViewport />
</ToastProvider>
)
}

67
tailwind.config.js Normal file
View File

@ -0,0 +1,67 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
// Montana Dinosaur Center colors
primary: "#23c4c8",
secondary: "#62e5f7",
background: "#f6eff6",
"mdc-dark": "#172b2d",
"mdc-blue": "#1f4e63",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
fontFamily: {
vollkorn: ["var(--font-vollkorn)"],
"open-sans": ["var(--font-open-sans)"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
}

86
utils/api.ts Normal file
View File

@ -0,0 +1,86 @@
"use server"
let authToken: string | null = null
let tokenExpiry: number | null = null
async function getAuthToken() {
// Check if we have a valid token
if (authToken && tokenExpiry && Date.now() < tokenExpiry) {
return authToken
}
// Get new token
try {
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: process.env.LUBELOGGER_USERNAME,
password: process.env.LUBELOGGER_PASSWORD,
}),
})
if (!response.ok) {
throw new Error("Authentication failed")
}
const data = await response.json()
authToken = data.token
// Set token expiry to 1 hour from now
tokenExpiry = Date.now() + 60 * 60 * 1000
return authToken
} catch (error) {
console.error("Authentication error:", error)
throw new Error("Failed to authenticate with LubeLogger API")
}
}
export async function fetchVehicles() {
const token = await getAuthToken()
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/vehicles`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error("Failed to fetch vehicles")
}
return response.json()
}
export async function submitFillup(data: {
name: string
vehicleId: string
mileage: number
gallons: number
}) {
const token = await getAuthToken()
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/fillups`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
employee: data.name,
vehicle_id: data.vehicleId,
odometer: data.mileage,
gallons: data.gallons,
date: new Date().toISOString(),
}),
})
if (!response.ok) {
const error = await response.json().catch(() => ({ message: "Failed to submit fillup" }))
throw new Error(error.message)
}
return response.json()
}