Initial commit
This commit is contained in:
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user