mirror of
https://github.com/tmdinosaurcenter/gas-form.git
synced 2025-04-23 12:22:26 -06:00
Starting from scratch. removed all previous attempt. Created fuel-entry.html
as an MVP form - fetches vehicles via GET /api/vehicles
to populate a <select>
. You can enter date, odometer, full-fill checkbox, notes & tags. On submit JSON sent to POST /api/vehicles/gasrecords/add?vehickeID=<id>
- Basic-Auth.
This commit is contained in:
parent
6e1e9368a6
commit
60e5bd7c44
54
README.md
54
README.md
@ -1,54 +0,0 @@
|
||||
# Gas Form
|
||||
|
||||
This repository contains the Gas Form System for the Montana Dinosaur Center. The system is designed to streamline the process of recording and managing fuel transactions for museum vehicles. The form submission links to TMDC's Lubelogger via API to track mileage and fill ups.
|
||||
|
||||
## Features
|
||||
|
||||
- Simple and user-friendly form for fuel transaction entries
|
||||
- Secure storage of fuel purchase records
|
||||
- Easy retrieval and reporting of fuel expenses
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To use or modify the Gas Form System, ensure you have:
|
||||
|
||||
- A modern web browser for web-based usage
|
||||
- [Git](https://git-scm.com/) installed for cloning and contributing
|
||||
- Basic knowledge of HTML, JavaScript, and server-side scripting (if modifications are needed)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/tmdinosaurcenter/gas-form.git
|
||||
cd gas-form
|
||||
```
|
||||
|
||||
2. Open `index.html` in a web browser to access the form.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Enter the required information, including:
|
||||
- Vehicle information
|
||||
- Mileage at time of fill up
|
||||
- Fuel quantity
|
||||
- Fuel cost
|
||||
|
||||
2. Submit the form to log the entry.
|
||||
|
||||
## Contribution
|
||||
|
||||
Contributions are welcome! To contribute:
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch `(git checkout -b feature-branch)`.
|
||||
3. Commit your changes `(git commit -m "Add new feature")`.
|
||||
4. Push to the branch `(git push origin feature-branch)`.
|
||||
5. Create a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE] file for details.
|
@ -1,27 +0,0 @@
|
||||
"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")
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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
12
app/page.tsx
@ -1,12 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import GasFillupForm from "@/components/GasFillupForm"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
<GasFillupForm />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
@ -1,232 +0,0 @@
|
||||
"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, Trash2 } from "lucide-react"
|
||||
|
||||
interface Vehicle {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function GasFillupForm() {
|
||||
const { toast } = useToast()
|
||||
|
||||
// Form state variables
|
||||
const [vehicle, setVehicle] = useState("")
|
||||
const [mileage, setMileage] = useState("")
|
||||
const [fuelAmount, setFuelAmount] = useState("")
|
||||
const [fuelCost, setFuelCost] = useState("")
|
||||
const [fuelReceipt, setFuelReceipt] = useState<File | null>(null) // Image file state
|
||||
const [previewURL, setPreviewURL] = useState<string | null>(null) // Image preview state
|
||||
|
||||
// API-related state
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// Fetch vehicles from API when the component loads
|
||||
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])
|
||||
|
||||
// Allowed image formats for uploads (common phone formats)
|
||||
const allowedFormats = ["image/png", "image/jpeg", "image/jpg", "image/heic", "image/heif", "image/webp"]
|
||||
|
||||
// Handle file selection and validation
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0] || null
|
||||
|
||||
if (file && !allowedFormats.includes(file.type)) {
|
||||
toast({
|
||||
title: "Invalid file type",
|
||||
description: "Please upload a valid image file (PNG, JPG, JPEG, HEIC, or WebP).",
|
||||
variant: "destructive",
|
||||
})
|
||||
setFuelReceipt(null) // Reset file selection
|
||||
setPreviewURL(null)
|
||||
return
|
||||
}
|
||||
|
||||
setFuelReceipt(file)
|
||||
|
||||
// Create an object URL for preview
|
||||
if (file) {
|
||||
const url = URL.createObjectURL(file)
|
||||
setPreviewURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await submitGasFillup({
|
||||
vehicleId: vehicle,
|
||||
mileage: Number(mileage),
|
||||
gallons: Number(fuelAmount),
|
||||
cost: Number(fuelCost),
|
||||
isFillToFull: true,
|
||||
missedFuelUp: false,
|
||||
file: fuelReceipt, // Attach uploaded file
|
||||
})
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Gas fillup has been recorded successfully.",
|
||||
})
|
||||
|
||||
// Reset form state after successful submission
|
||||
setVehicle("")
|
||||
setMileage("")
|
||||
setFuelAmount("")
|
||||
setFuelCost("")
|
||||
setFuelReceipt(null)
|
||||
setPreviewURL(null)
|
||||
} 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">
|
||||
{/* Form Header */}
|
||||
<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>
|
||||
|
||||
{/* Form Content */}
|
||||
<CardContent className="p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Vehicle Selection */}
|
||||
<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>
|
||||
|
||||
{/* Mileage Input */}
|
||||
<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>
|
||||
|
||||
{/* Fuel Amount Input */}
|
||||
<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>
|
||||
|
||||
{/* Fuel Cost Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fuelCost" className="text-mdc-dark">Fuel Cost ($)</Label>
|
||||
<Input
|
||||
id="fuelCost"
|
||||
type="number"
|
||||
value={fuelCost}
|
||||
onChange={(e) => setFuelCost(e.target.value)}
|
||||
required
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="border-mdc-blue focus:ring-secondary"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* File Upload Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fuelReceipt" className="text-mdc-dark">Upload Receipt (Optional)</Label>
|
||||
<Input
|
||||
id="fuelReceipt"
|
||||
type="file"
|
||||
accept=".png, .jpg, .jpeg, .heic, .heif, .webp"
|
||||
onChange={handleFileChange}
|
||||
className="border-mdc-blue focus:ring-secondary"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{/* Show image preview if a file is selected */}
|
||||
{previewURL && (
|
||||
<div className="relative mt-2">
|
||||
<img src={previewURL} alt="Receipt preview" className="w-full max-h-40 object-contain rounded-md" />
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-2 right-2 text-white bg-red-600 p-1 rounded-full"
|
||||
onClick={() => { setFuelReceipt(null); setPreviewURL(null) }}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<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>
|
||||
)
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { ButtonHTMLAttributes } from "react"
|
||||
|
||||
export function Button(props: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return <button {...props} className="px-4 py-2 bg-primary text-white rounded-md hover:bg-secondary transition-colors" />
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={`border border-gray-300 rounded-lg shadow-md p-4 ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
export function CardHeader({ children }: { children: React.ReactNode }) {
|
||||
return <div className="border-b border-gray-200 pb-2 mb-4 font-semibold">{children}</div>
|
||||
}
|
||||
|
||||
export function CardTitle({ children }: { children: React.ReactNode }) {
|
||||
return <h2 className="text-xl font-bold">{children}</h2>
|
||||
}
|
||||
|
||||
export function CardContent({ children }: { children: React.ReactNode }) {
|
||||
return <div className="mt-2">{children}</div>
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { InputHTMLAttributes } from "react"
|
||||
|
||||
export function Input(props: InputHTMLAttributes<HTMLInputElement>) {
|
||||
return <input {...props} className="border border-gray-300 p-2 rounded-md focus:outline-none focus:ring focus:ring-blue-500" />
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export function Label({ htmlFor, children }: { htmlFor: string; children: React.ReactNode }) {
|
||||
return <label htmlFor={htmlFor} className="block font-medium text-mdc-dark">{children}</label>
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
export function Select({ children, ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
return (
|
||||
<select {...props} className="border border-gray-300 p-2 rounded-md focus:outline-none focus:ring focus:ring-blue-500">
|
||||
{children}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export function SelectTrigger({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button {...props} className="border border-gray-300 p-2 rounded-md focus:outline-none focus:ring focus:ring-blue-500">
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function SelectContent({ children }: { children: React.ReactNode }) {
|
||||
return <div className="absolute bg-white shadow-md rounded-md p-2">{children}</div>
|
||||
}
|
||||
|
||||
export function SelectItem({ children, ...props }: React.OptionHTMLAttributes<HTMLOptionElement>) {
|
||||
return <option {...props}>{children}</option>
|
||||
}
|
||||
|
||||
export function SelectValue({ children }: { children: React.ReactNode }) {
|
||||
return <span>{children}</span>
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export const Toast = ({ children, className }: { children: React.ReactNode; className?: string }) => {
|
||||
return <div className={cn("fixed bottom-5 right-5 bg-gray-800 text-white p-3 rounded-lg shadow-lg", className)}>{children}</div>
|
||||
}
|
||||
|
||||
export const ToastTitle = ({ children }: { children: React.ReactNode }) => {
|
||||
return <strong className="block font-bold">{children}</strong>
|
||||
}
|
||||
|
||||
export const ToastDescription = ({ children }: { children: React.ReactNode }) => {
|
||||
return <p className="text-sm">{children}</p>
|
||||
}
|
||||
|
||||
export const ToastClose = ({ onClick }: { onClick: () => void }) => {
|
||||
return (
|
||||
<button onClick={onClick} className="absolute top-2 right-2 text-gray-400 hover:text-gray-200">
|
||||
✖
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const ToastViewport = () => {
|
||||
return <div className="fixed bottom-0 right-0 w-80 flex flex-col gap-2 p-4" />
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Toast, ToastClose, ToastDescription, ToastTitle, ToastViewport } from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { message } = useToast()
|
||||
|
||||
return message ? (
|
||||
<Toast>
|
||||
<ToastTitle>Notification</ToastTitle>
|
||||
<ToastDescription>{message}</ToastDescription>
|
||||
<ToastClose onClick={() => console.log("Close Toast")} />
|
||||
<ToastViewport />
|
||||
</Toast>
|
||||
) : null
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
export function useToast() {
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
const toast = ({ title, description, variant }: { title: string; description: string; variant?: "default" | "destructive" }) => {
|
||||
console.log(`${variant === "destructive" ? "[ERROR]" : "[INFO]"} ${title}: ${description}`)
|
||||
setMessage(`${title}: ${description}`)
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
}
|
||||
|
||||
return { toast, message }
|
||||
}
|
139
fuel-entry.html
Normal file
139
fuel-entry.html
Normal file
@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Add Fuel Record</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Add Fuel Record</h1>
|
||||
|
||||
<form id="fuelForm">
|
||||
<label>
|
||||
Vehicle:
|
||||
<select id="vehicleSelect" required>
|
||||
<option value="">Loading…</option>
|
||||
</select>
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Date:
|
||||
<input type="date" id="date" required>
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Odometer:
|
||||
<input type="number" id="odometer" required>
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Gallons (fuelConsumed):
|
||||
<input type="number" step="0.01" id="fuelConsumed" required>
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Cost:
|
||||
<input type="number" step="0.01" id="cost" required>
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Fill to full:
|
||||
<input type="checkbox" id="isFillToFull">
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Notes:
|
||||
<textarea id="notes"></textarea>
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<label>
|
||||
Tags (comma‑separated):
|
||||
<input type="text" id="tags">
|
||||
</label>
|
||||
<br /><br />
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<div id="message"></div>
|
||||
|
||||
<script>
|
||||
// ** CONFIGURE **
|
||||
const BASE_URL = 'https://cars.dogiakos.com';
|
||||
const USERNAME = 'YOUR_USERNAME';
|
||||
const PASSWORD = 'YOUR_PASSWORD';
|
||||
|
||||
// helper to format date as MM/DD/YYYY for locale-sensitive API
|
||||
function fmtDate(d) {
|
||||
const [y, m, day] = d.split('-');
|
||||
return `${m}/${day}/${y}`;
|
||||
}
|
||||
|
||||
// populate vehicles dropdown
|
||||
async function loadVehicles() {
|
||||
const res = await fetch(`${BASE_URL}/api/vehicles`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + btoa(USERNAME + ':' + PASSWORD)
|
||||
}
|
||||
});
|
||||
const vehicles = await res.json();
|
||||
const sel = document.getElementById('vehicleSelect');
|
||||
sel.innerHTML = '<option value="">Select…</option>';
|
||||
vehicles.forEach(v => {
|
||||
sel.innerHTML += `<option value="${v.id}">
|
||||
${v.year} ${v.make} ${v.model}
|
||||
</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('fuelForm')
|
||||
.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const vid = document.getElementById('vehicleSelect').value;
|
||||
if (!vid) return alert('Pick a vehicle');
|
||||
|
||||
const payload = {
|
||||
date: fmtDate(document.getElementById('date').value),
|
||||
odometer: Number(document.getElementById('odometer').value),
|
||||
fuelConsumed: Number(document.getElementById('fuelConsumed').value),
|
||||
cost: Number(document.getElementById('cost').value),
|
||||
isFillToFull: document.getElementById('isFillToFull').checked,
|
||||
notes: document.getElementById('notes').value,
|
||||
tags: document.getElementById('tags').value,
|
||||
// you can add missedFuelUp, extraFields, etc.
|
||||
};
|
||||
|
||||
const submitUrl = `${BASE_URL}/api/vehicle/gasrecords/add?vehicleId=${vid}`;
|
||||
const res = await fetch(submitUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic ' + btoa(USERNAME + ':' + PASSWORD)
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const msg = document.getElementById('message');
|
||||
if (res.ok) {
|
||||
msg.textContent = '✅ Fuel record added!';
|
||||
e.target.reset();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
msg.textContent = `❌ Error (${res.status}): ${txt}`;
|
||||
}
|
||||
});
|
||||
|
||||
// kick it off
|
||||
loadVehicles();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,3 +0,0 @@
|
||||
export function cn(...classes: (string | undefined | null | false)[]): string {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
5
next-env.d.ts
vendored
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
7055
package-lock.json
generated
7055
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "gas-form",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.3",
|
||||
"lucide-react": "^0.310.0",
|
||||
"next": "^15.1.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.0.8",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-config-next": "^14.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"shadcn-ui": "^0.9.4",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/** @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")],
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".", // ✅ Add this
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/lib/*": ["lib/*"],
|
||||
"@/components/*": ["components/*"],
|
||||
"@/components/ui/*": ["components/ui/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
95
utils/api.ts
95
utils/api.ts
@ -1,95 +0,0 @@
|
||||
"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
|
||||
cost: number
|
||||
isFillToFull?: boolean
|
||||
missedFuelUp?: boolean
|
||||
notes?: string
|
||||
tags?: string
|
||||
}) {
|
||||
const token = await getAuthToken()
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("vehicleId", data.vehicleId)
|
||||
formData.append("date", new Date().toISOString()) // Send current date/time
|
||||
formData.append("odometer", data.mileage.toString())
|
||||
formData.append("fuelConsumed", data.gallons.toString()) // Correct field name
|
||||
formData.append("cost", data.cost.toString()) // Required by API
|
||||
formData.append("isFillToFull", (data.isFillToFull ?? true).toString()) // Default: true
|
||||
formData.append("missedFuelUp", (data.missedFuelUp ?? false).toString()) // Default: false
|
||||
|
||||
if (data.notes) formData.append("notes", data.notes)
|
||||
if (data.tags) formData.append("tags", data.tags)
|
||||
|
||||
const response = await fetch(`${process.env.LUBELOGGER_API_URL}/api/vehicle/gasrecords/add`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData, // Use form-data instead of JSON
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: "Failed to submit fillup" }))
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user