4b60ee0e-4981-4301-aad2-220e7e9c760a — Commit 5d827618
Changed files
.env.development | 1 + .env.production | 1 + .env.staging | 1 + Dockerfile | 29 +++++++++++++++++++++++++++++ docker-compose.yml | 8 ++++++++ package.json | 3 +++ src/api/client.ts | 3 +-- src/components/AppLayout.vue | 32 +++++++++++++++++++++++++++++++- src/components/DeeplinkCard.vue | 2 +- src/components/OrgSidebar.vue | 8 +++++++- src/env.d.ts | 9 +++++++++ src/stores/organizations.ts | 12 ++++++++++++ vite.config.ts | 9 +++++++++ 13 files changed, 113 insertions(+), 5 deletions(-)
Diff
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..3d862c1
--- /dev/null
+++ b/.env.development
@@ -0,0 +1 @@
+VITE_API_BASE_URL=http://localhost:3200
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..35e6ed3
--- /dev/null
+++ b/.env.production
@@ -0,0 +1 @@
+VITE_API_BASE_URL=https://api.diver.mthy.dev
diff --git a/.env.staging b/.env.staging
new file mode 100644
index 0000000..3d862c1
--- /dev/null
+++ b/.env.staging
@@ -0,0 +1 @@
+VITE_API_BASE_URL=http://localhost:3200
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4ff441e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+# Build stage
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+COPY package.json package-lock.json* ./
+RUN npm ci
+
+COPY . .
+RUN npm run build:staging
+
+# Serve stage
+FROM nginx:alpine
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Handle SPA routing
+RUN printf 'server {\n\
+ listen 80;\n\
+ root /usr/share/nginx/html;\n\
+ index index.html;\n\
+ location / {\n\
+ try_files $uri $uri/ /index.html;\n\
+ }\n\
+}\n' > /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..dfe8199
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,8 @@
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "3291:80"
+ restart: unless-stopped
diff --git a/package.json b/package.json
index 464631e..4850c18 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"type": "module",
"scripts": {
"dev": "vite",
+ "dev:staging": "vite --mode staging",
"build": "vue-tsc && vite build",
+ "build:staging": "vue-tsc && vite build --mode staging",
+ "build:production": "vue-tsc && vite build --mode production",
"preview": "vite preview"
},
"dependencies": {
diff --git a/src/api/client.ts b/src/api/client.ts
index 315b2a1..e0c9346 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -2,8 +2,7 @@ import axios from 'axios'
import type { Organization, App, DeeplinkTemplate } from '@/types'
const api = axios.create({
- //baseURL: 'https://api.diver.mthy.dev',
- baseURL: 'localhost:3300',
+ baseURL: import.meta.env.VITE_API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
diff --git a/src/components/AppLayout.vue b/src/components/AppLayout.vue
index b30dc75..4f8dfd6 100644
--- a/src/components/AppLayout.vue
+++ b/src/components/AppLayout.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useThemeStore } from '@/stores/theme'
import OrgSidebar from './OrgSidebar.vue'
@@ -8,6 +8,13 @@ const themeStore = useThemeStore()
const router = useRouter()
const sidebarOpen = ref(false)
+const mode = import.meta.env.MODE
+const envBadge = computed(() => {
+ if (mode === 'production') return null
+ if (mode === 'staging') return { label: 'staging', class: 'env-badge--staging' }
+ return { label: 'dev', class: 'env-badge--dev' }
+})
+
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
@@ -33,6 +40,7 @@ function goHome() {
<span class="logo-icon">🤿</span>
<span class="logo-text">Diver</span>
</button>
+ <span v-if="envBadge" :class="['env-badge', envBadge.class]">{{ envBadge.label }}</span>
</div>
<div class="header-right">
<RouterLink to="/launch-history" class="btn btn-ghost btn-sm history-link">
@@ -124,6 +132,28 @@ function goHome() {
font-size: 22px;
}
+.env-badge {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ padding: 2px 7px;
+ border-radius: 10px;
+ line-height: 1.4;
+}
+
+.env-badge--dev {
+ background: #2563eb22;
+ color: #3b82f6;
+ border: 1px solid #3b82f640;
+}
+
+.env-badge--staging {
+ background: #d9770622;
+ color: #f59e0b;
+ border: 1px solid #f59e0b40;
+}
+
.history-link {
display: flex;
align-items: center;
diff --git a/src/components/DeeplinkCard.vue b/src/components/DeeplinkCard.vue
index 50a823a..dbc5525 100644
--- a/src/components/DeeplinkCard.vue
+++ b/src/components/DeeplinkCard.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import type { DeeplinkTemplate } from '@/types'
+import type {DeeplinkTemplate} from '@/types'
const props = defineProps<{
deeplink: DeeplinkTemplate
diff --git a/src/components/OrgSidebar.vue b/src/components/OrgSidebar.vue
index 54c4ee8..a90882f 100644
--- a/src/components/OrgSidebar.vue
+++ b/src/components/OrgSidebar.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
-import { computed } from 'vue'
+import { computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useOrganizationsStore } from '@/stores/organizations'
@@ -31,6 +31,12 @@ function removeOrg(e: Event, orgId: string) {
}
}
+onMounted(() => {
+ if (!import.meta.env.PROD) {
+ orgStore.fetchAllOrganizations()
+ }
+})
+
async function copyOrgId(e: Event, orgId: string) {
e.stopPropagation()
await navigator.clipboard.writeText(orgId)
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..8ab4f00
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,9 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+ readonly VITE_API_BASE_URL: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/src/stores/organizations.ts b/src/stores/organizations.ts
index 5d0ccb0..694929a 100644
--- a/src/stores/organizations.ts
+++ b/src/stores/organizations.ts
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Organization, App } from '@/types'
+import { getOrganizations } from '@/api/client'
const STORAGE_KEY = 'diver_organizations'
@@ -60,6 +61,16 @@ export const useOrganizationsStore = defineStore('organizations', () => {
apps.value = apps.value.filter(a => a.id !== appId)
}
+ async function fetchAllOrganizations() {
+ const all = await getOrganizations()
+ for (const org of all) {
+ if (!organizations.value.find(o => o.id === org.id)) {
+ organizations.value.push(org)
+ }
+ }
+ saveToStorage(organizations.value)
+ }
+
function addApp(app: App) {
const idx = apps.value.findIndex(a => a.id === app.id)
if (idx >= 0) {
@@ -83,5 +94,6 @@ export const useOrganizationsStore = defineStore('organizations', () => {
setAppsError,
removeApp,
addApp,
+ fetchAllOrganizations,
}
})
diff --git a/vite.config.ts b/vite.config.ts
index 25ccbc8..3923f95 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -9,4 +9,13 @@ export default defineConfig({
'@': resolve(__dirname, 'src'),
},
},
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3200',
+ changeOrigin: true,
+ rewrite: path => path.replace(/^\/api/, ''),
+ },
+ },
+ },
})