4daebf8c-dc42-4579-b104-0ed8afe17301 — Commit 74ea908b
Changed files
package.json | 1 + src/App.vue | 4 +-- src/components/AppCard.vue | 65 ----------------------------------------- src/components/OrgSidebar.vue | 30 +++++++++++-------- src/views/AddAppView.vue | 51 +++++++++++++++++++++++++++----- src/views/AppDetailView.vue | 37 +++++++++++++++++------ src/views/HomeView.vue | 61 ++++++++++++++++---------------------- src/views/LaunchHistoryView.vue | 41 +++++++++++++------------- 8 files changed, 138 insertions(+), 152 deletions(-)
Diff
diff --git a/package.json b/package.json
index 0f41b44..c546c10 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"dev:staging": "vite --mode staging",
+ "dev:production": "vite --mode production",
"build": "vue-tsc && vite build",
"build:staging": "vue-tsc && vite build --mode staging",
"build:production": "vue-tsc && vite build --mode production",
diff --git a/src/App.vue b/src/App.vue
index 4e63959..a02fabb 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,9 +1,9 @@
<script setup lang="ts">
-import { useThemeStore } from '@/stores/theme'
+import {useThemeStore} from '@/stores/theme'
// Initialize theme store on app mount
useThemeStore()
</script>
<template>
- <RouterView />
+ <RouterView/>
</template>
diff --git a/src/components/AppCard.vue b/src/components/AppCard.vue
index 9fa10d4..eff1000 100644
--- a/src/components/AppCard.vue
+++ b/src/components/AppCard.vue
@@ -7,25 +7,11 @@ const props = defineProps<{
orgId: string
}>()
-const emit = defineEmits<{
- delete: [appId: string]
-}>()
-
const router = useRouter()
function open() {
router.push(`/org/${props.orgId}/app/${props.app.id}`)
}
-
-function editApp(e: Event) {
- e.stopPropagation()
- router.push(`/org/${props.orgId}/app/${props.app.id}/edit-app`)
-}
-
-function deleteApp(e: Event) {
- e.stopPropagation()
- emit('delete', props.app.id)
-}
</script>
<template>
@@ -43,22 +29,6 @@ function deleteApp(e: Event) {
</div>
</div>
</div>
- <div class="app-card-actions">
- <button class="action-btn" title="Edit app" @click="editApp">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
- <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
- </svg>
- </button>
- <button class="action-btn action-btn-danger" title="Delete app" @click="deleteApp">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <polyline points="3 6 5 6 21 6"/>
- <path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/>
- <path d="M10 11v6M14 11v6"/>
- <path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/>
- </svg>
- </button>
- </div>
</div>
</template>
@@ -136,39 +106,4 @@ function deleteApp(e: Event) {
font-size: 12px;
color: var(--color-text-muted);
}
-
-.app-card-actions {
- display: flex;
- gap: 4px;
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.app-card:hover .app-card-actions {
- opacity: 1;
-}
-
-.action-btn {
- width: 30px;
- height: 30px;
- border-radius: 6px;
- border: none;
- background: var(--color-surface-raised);
- color: var(--color-text-muted);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: background 0.12s, color 0.12s;
-}
-
-.action-btn:hover {
- background: var(--color-border);
- color: var(--color-text);
-}
-
-.action-btn-danger:hover {
- background: rgba(239, 68, 68, 0.1);
- color: var(--color-error);
-}
</style>
diff --git a/src/components/OrgSidebar.vue b/src/components/OrgSidebar.vue
index 4873a2c..8fa022a 100644
--- a/src/components/OrgSidebar.vue
+++ b/src/components/OrgSidebar.vue
@@ -1,7 +1,7 @@
<script setup lang="ts">
import {computed, onMounted, ref} from 'vue'
-import { useRouter, useRoute } from 'vue-router'
-import { useOrganizationsStore } from '@/stores/organizations'
+import {useRouter, useRoute} from 'vue-router'
+import {useOrganizationsStore} from '@/stores/organizations'
const emit = defineEmits<{ navigate: [] }>()
@@ -61,10 +61,10 @@ async function copyOrgId(e: Event, orgId: string) {
<div class="org-list">
<div
- v-for="org in orgStore.organizations"
- :key="org.id"
- :class="['org-item', { active: currentOrgId === org.id }]"
- @click="selectOrg(org.id)"
+ v-for="org in orgStore.organizations"
+ :key="org.id"
+ :class="['org-item', { active: currentOrgId === org.id }]"
+ @click="selectOrg(org.id)"
>
<div class="org-avatar">{{ org.name.charAt(0).toUpperCase() }}</div>
<div class="org-info">
@@ -73,9 +73,9 @@ async function copyOrgId(e: Event, orgId: string) {
</div>
<div class="org-actions">
<button
- class="action-btn"
- title="Copy ID"
- @click="copyOrgId($event, org.id)"
+ class="action-btn"
+ title="Copy ID"
+ @click="copyOrgId($event, org.id)"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
@@ -83,9 +83,9 @@ async function copyOrgId(e: Event, orgId: string) {
</svg>
</button>
<button
- class="action-btn action-btn-danger"
- title="Remove"
- @click="removeOrg($event, org.id)"
+ class="action-btn action-btn-danger"
+ title="Remove"
+ @click="removeOrg($event, org.id)"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
@@ -211,6 +211,12 @@ async function copyOrgId(e: Event, orgId: string) {
display: flex;
}
+@media (hover: none) {
+ .org-actions {
+ display: flex;
+ }
+}
+
.action-btn {
width: 24px;
height: 24px;
diff --git a/src/views/AddAppView.vue b/src/views/AddAppView.vue
index 4720147..2152ced 100644
--- a/src/views/AddAppView.vue
+++ b/src/views/AddAppView.vue
@@ -2,7 +2,7 @@
import {ref, onMounted, computed} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {useOrganizationsStore} from '@/stores/organizations'
-import {createApp, getApp, updateApp} from '@/api/client'
+import {createApp, deleteApp, getApp, updateApp} from '@/api/client'
import type {Environment} from '@/types'
import AppLayout from '@/components/AppLayout.vue'
import EnvironmentEditor from '@/components/EnvironmentEditor.vue'
@@ -18,6 +18,7 @@ const isEdit = computed(() => !!appId)
const name = ref('')
const environments = ref<Environment[]>([{name: '', scheme: ''}])
const loading = ref(false)
+const deleting = ref(false)
const error = ref<string | null>(null)
const initialLoading = ref(false)
@@ -88,6 +89,22 @@ async function handleSubmit() {
const backUrl = computed(() =>
isEdit.value && appId ? `/org/${orgId}/app/${appId}` : `/org/${orgId}`
)
+
+async function handleDelete() {
+ if (!appId) return
+ if (!confirm('Delete this app? This cannot be undone.')) return
+ deleting.value = true
+ error.value = null
+ try {
+ await deleteApp(orgId, appId)
+ orgStore.removeApp(appId)
+ router.push(`/org/${orgId}`)
+ } catch (e: unknown) {
+ error.value = e instanceof Error ? e.message : 'Failed to delete app'
+ } finally {
+ deleting.value = false
+ }
+}
</script>
<template>
@@ -131,15 +148,26 @@ const backUrl = computed(() =>
</div>
<div class="form-actions">
- <RouterLink :to="backUrl" class="btn btn-secondary">Cancel</RouterLink>
<button
- type="submit"
- class="btn btn-primary"
- :disabled="loading"
+ v-if="isEdit"
+ type="button"
+ class="btn btn-danger"
+ :disabled="deleting || loading"
+ @click="handleDelete"
>
- <div v-if="loading" class="spinner" style="width:14px;height:14px;border-width:2px"></div>
- {{ loading ? 'Saving…' : (isEdit ? 'Save Changes' : 'Create App') }}
+ {{ deleting ? 'Deleting…' : 'Delete App' }}
</button>
+ <div class="form-actions-right">
+ <RouterLink :to="backUrl" class="btn btn-secondary">Cancel</RouterLink>
+ <button
+ type="submit"
+ class="btn btn-primary"
+ :disabled="loading || deleting"
+ >
+ <div v-if="loading" class="spinner" style="width:14px;height:14px;border-width:2px"></div>
+ {{ loading ? 'Saving…' : (isEdit ? 'Save Changes' : 'Create App') }}
+ </button>
+ </div>
</div>
</form>
</div>
@@ -207,7 +235,14 @@ const backUrl = computed(() =>
.form-actions {
display: flex;
gap: 10px;
- justify-content: flex-end;
+ justify-content: space-between;
+ align-items: center;
margin-top: 8px;
}
+
+.form-actions-right {
+ display: flex;
+ gap: 10px;
+ margin-left: auto;
+}
</style>
diff --git a/src/views/AppDetailView.vue b/src/views/AppDetailView.vue
index e313062..b36424c 100644
--- a/src/views/AppDetailView.vue
+++ b/src/views/AppDetailView.vue
@@ -115,15 +115,28 @@ function closeLaunchModal() {
</button>
</div>
</div>
- <RouterLink
- :to="`/org/${orgId}/app/${appId}/add-deeplink`"
- class="btn btn-primary"
- >
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
- <path d="M12 5v14M5 12h14"/>
- </svg>
- Add Deeplink
- </RouterLink>
+ <div class="header-actions">
+ <RouterLink
+ :to="`/org/${orgId}/app/${appId}/edit-app`"
+ class="btn btn-secondary"
+ title="Edit app"
+ >
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
+ </svg>
+ Edit
+ </RouterLink>
+ <RouterLink
+ :to="`/org/${orgId}/app/${appId}/add-deeplink`"
+ class="btn btn-primary"
+ >
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
+ <path d="M12 5v14M5 12h14"/>
+ </svg>
+ Add Deeplink
+ </RouterLink>
+ </div>
</div>
<!-- Empty state -->
@@ -222,6 +235,12 @@ function closeLaunchModal() {
gap: 6px;
}
+.header-actions {
+ display: flex;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
.env-chip {
font-size: 11px;
font-weight: 600;
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index fd81f2e..2653d85 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -1,8 +1,8 @@
<script setup lang="ts">
-import { watch, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { useOrganizationsStore } from '@/stores/organizations'
-import { getApps, deleteApp } from '@/api/client'
+import {watch, onMounted} from 'vue'
+import {useRoute, useRouter} from 'vue-router'
+import {useOrganizationsStore} from '@/stores/organizations'
+import {getApps} from '@/api/client'
import AppLayout from '@/components/AppLayout.vue'
import AppCard from '@/components/AppCard.vue'
@@ -13,7 +13,7 @@ const orgStore = useOrganizationsStore()
const orgId = () => route.params.orgId as string | undefined
const currentOrg = () =>
- orgId() ? orgStore.organizations.find(o => o.id === orgId()) : null
+ orgId() ? orgStore.organizations.find(o => o.id === orgId()) : null
async function loadApps(id: string) {
orgStore.setAppsLoading(true)
@@ -23,7 +23,7 @@ async function loadApps(id: string) {
orgStore.setApps(apps)
} catch (e: unknown) {
orgStore.setAppsError(
- e instanceof Error ? e.message : 'Failed to load apps'
+ e instanceof Error ? e.message : 'Failed to load apps'
)
} finally {
orgStore.setAppsLoading(false)
@@ -31,16 +31,16 @@ async function loadApps(id: string) {
}
watch(
- () => route.params.orgId,
- (id) => {
- if (id) {
- orgStore.setCurrentOrg(id as string)
- loadApps(id as string)
- } else {
- orgStore.setCurrentOrg(null)
- orgStore.setApps([])
+ () => route.params.orgId,
+ (id) => {
+ if (id) {
+ orgStore.setCurrentOrg(id as string)
+ loadApps(id as string)
+ } else {
+ orgStore.setCurrentOrg(null)
+ orgStore.setApps([])
+ }
}
- }
)
onMounted(() => {
@@ -51,17 +51,6 @@ onMounted(() => {
}
})
-async function handleDeleteApp(appId: string) {
- const id = orgId()
- if (!id) return
- if (!confirm('Delete this app? This cannot be undone.')) return
- try {
- await deleteApp(id, appId)
- orgStore.removeApp(appId)
- } catch {
- alert('Failed to delete app')
- }
-}
</script>
<template>
@@ -92,8 +81,8 @@ async function handleDeleteApp(appId: string) {
<div class="page-subtitle">Apps</div>
</div>
<RouterLink
- :to="`/org/${orgId()}/add-app`"
- class="btn btn-primary"
+ :to="`/org/${orgId()}/add-app`"
+ class="btn btn-primary"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 5v14M5 12h14"/>
@@ -119,9 +108,9 @@ async function handleDeleteApp(appId: string) {
<h3>No apps yet</h3>
<p>Add your first app to start managing deeplinks.</p>
<RouterLink
- :to="`/org/${orgId()}/add-app`"
- class="btn btn-primary"
- style="margin-top: 16px; display: inline-flex;"
+ :to="`/org/${orgId()}/add-app`"
+ class="btn btn-primary"
+ style="margin-top: 16px; display: inline-flex;"
>
Add App
</RouterLink>
@@ -130,11 +119,10 @@ async function handleDeleteApp(appId: string) {
<!-- App list -->
<div v-else class="apps-grid">
<AppCard
- v-for="app in orgStore.apps"
- :key="app.id"
- :app="app"
- :orgId="orgId()!"
- @delete="handleDeleteApp"
+ v-for="app in orgStore.apps"
+ :key="app.id"
+ :app="app"
+ :orgId="orgId()!"
/>
</div>
</template>
@@ -179,6 +167,7 @@ async function handleDeleteApp(appId: string) {
.content-header {
padding: 16px 16px 12px;
}
+
.apps-grid {
padding: 0 16px 16px;
}
diff --git a/src/views/LaunchHistoryView.vue b/src/views/LaunchHistoryView.vue
index 68d2d9b..4fceafc 100644
--- a/src/views/LaunchHistoryView.vue
+++ b/src/views/LaunchHistoryView.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
-import { computed } from 'vue'
-import { useHistoryStore } from '@/stores/history'
+import {computed} from 'vue'
+import {useHistoryStore} from '@/stores/history'
import AppLayout from '@/components/AppLayout.vue'
const historyStore = useHistoryStore()
@@ -35,12 +35,12 @@ const grouped = computed(() => {
const seen: Record<string, number> = {}
for (const entry of historyStore.entries) {
- const date = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(
- new Date(entry.timestamp)
+ const date = new Intl.DateTimeFormat(undefined, {dateStyle: 'medium'}).format(
+ new Date(entry.timestamp)
)
if (seen[date] === undefined) {
seen[date] = groups.length
- groups.push({ date, entries: [] })
+ groups.push({date, entries: []})
}
groups[seen[date]].entries.push(entry)
}
@@ -58,9 +58,9 @@ const grouped = computed(() => {
<div class="page-subtitle">{{ historyStore.entries.length }} launches recorded</div>
</div>
<button
- v-if="historyStore.entries.length > 0"
- class="btn btn-danger btn-sm"
- @click="clearHistory"
+ v-if="historyStore.entries.length > 0"
+ class="btn btn-danger btn-sm"
+ @click="clearHistory"
>
Clear All
</button>
@@ -76,16 +76,16 @@ const grouped = computed(() => {
<!-- History grouped by date -->
<div v-else class="history-content">
<div
- v-for="group in grouped"
- :key="group.date"
- class="history-group"
+ v-for="group in grouped"
+ :key="group.date"
+ class="history-group"
>
<div class="group-date">{{ group.date }}</div>
<div class="entries-list">
<div
- v-for="entry in group.entries"
- :key="entry.id"
- class="history-entry"
+ v-for="entry in group.entries"
+ :key="entry.id"
+ class="history-entry"
>
<div class="entry-main">
<div class="entry-header">
@@ -97,9 +97,9 @@ const grouped = computed(() => {
</div>
<div class="entry-actions">
<button
- class="entry-btn"
- title="Copy URI"
- @click="copyUri(entry.uri)"
+ class="entry-btn"
+ title="Copy URI"
+ @click="copyUri(entry.uri)"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
@@ -107,9 +107,9 @@ const grouped = computed(() => {
</svg>
</button>
<button
- class="entry-btn launch-btn"
- title="Re-launch"
- @click="relaunch(entry.uri)"
+ class="entry-btn launch-btn"
+ title="Re-launch"
+ @click="relaunch(entry.uri)"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M5 12h14M12 5l7 7-7 7"/>
@@ -271,6 +271,7 @@ const grouped = computed(() => {
.content-header {
padding: 16px 16px 12px;
}
+
.history-content {
padding: 0 16px 16px;
}