Initial commit

This commit is contained in:
2026-05-12 15:40:22 -06:00
parent 5b279865a1
commit 1eac72b3cd
31 changed files with 1192 additions and 45 deletions
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LogJensticks</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
{
"name": "logjensticks-frontend",
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
<template>
<RouterView />
</template>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
+49
View File
@@ -0,0 +1,49 @@
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
import Carrier from '../views/Carrier.vue'
import CreateLane from '../views/CreateLane.vue'
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{ path: '/broker', component: CreateLane, meta: { requiresAuth: true, role: 'broker' } },
{ path: '/carrier', component: Carrier, meta: { requiresAuth: true, role: 'carrier' } },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// Fetch the current session once per navigation.
async function getSession() {
const res = await fetch('/me')
if (!res.ok) return null
const { data } = await res.json()
return data
}
function homeForRole(role) {
return role === 'broker' ? '/broker' : '/carrier'
}
router.beforeEach(async (to) => {
const session = await getSession()
if (to.path === '/login') {
// Already logged in — send to the right home page.
if (session) return homeForRole(session.role)
return true
}
if (!session) return '/login'
// Logged in but landed on the wrong role's page.
if (to.meta.role && session.role !== to.meta.role) {
return homeForRole(session.role)
}
return true
})
export default router
+16
View File
@@ -0,0 +1,16 @@
<template>
<h1>Carrier</h1>
<p>You are logged in.</p>
<button @click="logout">Log Out</button>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
async function logout() {
await fetch('/logout', { method: 'POST' })
router.push('/login')
}
</script>
+86
View File
@@ -0,0 +1,86 @@
<template>
<h1>Create Lane</h1>
<form v-if="!createdLink" @submit.prevent="submit">
<fieldset>
<legend>Internal Reference</legend>
<label for="laneId">Lane ID</label><br>
<input id="laneId" v-model="form.laneId" type="text" placeholder="e.g. LN-2094">
</fieldset>
<fieldset>
<legend>State Codes</legend>
<label for="pickupState">Pickup State</label><br>
<input id="pickupState" v-model="form.pickupState" type="text" maxlength="2" placeholder="e.g. CA">
<br><br>
<label for="dropoffState">Dropoff State</label><br>
<input id="dropoffState" v-model="form.dropoffState" type="text" maxlength="2" placeholder="e.g. TX">
</fieldset>
<fieldset>
<legend>Addresses</legend>
<label for="pickupAddress">Pickup Address</label><br>
<input id="pickupAddress" v-model="form.pickupAddress" type="text" placeholder="123 Warehouse Blvd, Los Angeles, CA 90001">
<br><br>
<label for="dropoffAddress">Dropoff Address</label><br>
<input id="dropoffAddress" v-model="form.dropoffAddress" type="text" placeholder="456 Distribution Dr, Houston, TX 77001">
</fieldset>
<p v-if="error" style="color:red">{{ error }}</p>
<button type="submit">Create Lane</button>
</form>
<div v-else>
<p>Lane created. Share this link with carriers:</p>
<a :href="createdLink">{{ createdLink }}</a>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const form = reactive({
laneId: '',
pickupState: '',
dropoffState: '',
pickupAddress: '',
dropoffAddress: '',
})
const error = ref('')
const createdLink = ref('')
const hasLaneId = () => form.laneId.trim() !== ''
const hasStateCodes = () => form.pickupState.trim() !== '' && form.dropoffState.trim() !== ''
const hasAddresses = () => form.pickupAddress.trim() !== '' && form.dropoffAddress.trim() !== ''
async function submit() {
error.value = ''
if (!hasLaneId() && !hasStateCodes() && !hasAddresses()) {
error.value = 'Provide at least a Lane ID, both state codes, or both addresses.'
return
}
const res = await fetch('/lanes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
lane_id: form.laneId || null,
pickup_state: form.pickupState || null,
dropoff_state: form.dropoffState || null,
pickup_address: form.pickupAddress || null,
dropoff_address: form.dropoffAddress || null,
}),
})
if (!res.ok) {
const body = await res.json()
error.value = body.error?.message ?? 'Failed to create lane.'
return
}
const { data } = await res.json()
createdLink.value = `${window.location.origin}/lanes/${data.id}`
}
</script>
+43
View File
@@ -0,0 +1,43 @@
<template>
<h1>LogJensticks</h1>
<form @submit.prevent="submit">
<div>
<label for="username">Username</label><br>
<input id="username" v-model="username" type="text" required autofocus>
</div>
<div>
<label for="password">Password</label><br>
<input id="password" v-model="password" type="password" required>
</div>
<div>
<button type="submit">Log In</button>
</div>
<p v-if="error" style="color:red">{{ error }}</p>
</form>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const username = ref('')
const password = ref('')
const error = ref('')
async function submit() {
error.value = ''
const res = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username.value, password: password.value }),
})
if (res.ok) {
const { data } = await res.json()
router.push(data.role === 'broker' ? '/broker' : '/carrier')
} else {
error.value = 'Invalid username or password.'
}
}
</script>
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
// Proxy API calls to the Go server during local development.
proxy: {
'/login': 'http://localhost:8080',
'/logout': 'http://localhost:8080',
'/me': 'http://localhost:8080',
'/health': 'http://localhost:8080',
},
},
})