refactor: update app.vue and various components to enhance UI consistency, replace color classes for improved accessibility, and refine layout for better user experience
This commit is contained in:
parent
7b4fb6c2fd
commit
24e8b7a3a8
41 changed files with 2395 additions and 1603 deletions
|
|
@ -2,139 +2,156 @@
|
|||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<!-- Section Header -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-2xl font-black text-black mb-2">
|
||||
<h3 class="text-2xl font-black text-black dark:text-white mb-2">
|
||||
Where will your money come from?
|
||||
</h3>
|
||||
<p class="text-neutral-600">
|
||||
<p class="text-neutral-600 dark:text-neutral-200">
|
||||
Add sources like client work, grants, product sales, or donations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Removed Tab Navigation - showing streams directly -->
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-if="streams.length === 0"
|
||||
class="text-center py-12 border-2 border-dashed border-neutral-300 dark:border-neutral-600 rounded-xl bg-white dark:bg-neutral-950 shadow-sm">
|
||||
<h4 class="font-medium text-neutral-900 dark:text-neutral-100 mb-2">
|
||||
No revenue streams yet
|
||||
</h4>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
||||
Get started by adding your first revenue source.
|
||||
</p>
|
||||
<UButton
|
||||
@click="addRevenueStream"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
color="primary">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2" />
|
||||
Add your first revenue stream
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-if="streams.length === 0"
|
||||
class="text-center py-12 border-2 border-dashed border-neutral-300 rounded-xl bg-white shadow-sm">
|
||||
<h4 class="font-medium text-neutral-900 mb-2">
|
||||
No revenue streams yet
|
||||
</h4>
|
||||
<p class="text-sm text-neutral-500 mb-4">
|
||||
Get started by adding your first revenue source.
|
||||
<div
|
||||
v-for="stream in streams"
|
||||
:key="stream.id"
|
||||
class="p-6 border-2 border-black dark:border-neutral-400 rounded-xl bg-white dark:bg-neutral-950 shadow-md">
|
||||
<!-- First row: Category and Name with delete button -->
|
||||
<div class="flex gap-4 mb-4">
|
||||
<UFormField label="Category" required class="flex-1">
|
||||
<USelect
|
||||
v-model="stream.category"
|
||||
:items="categoryOptions"
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="saveCategoryChange(stream)" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Name" required class="flex-1">
|
||||
<div class="flex gap-2">
|
||||
<USelectMenu
|
||||
v-model="stream.name"
|
||||
:items="nameOptionsByCategory[stream.category] || []"
|
||||
placeholder="Select or type a source name"
|
||||
creatable
|
||||
searchable
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="saveStream(stream)" />
|
||||
<UButton
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="error"
|
||||
@click="removeStream(stream.id)"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:opacity-90 transition-opacity',
|
||||
}">
|
||||
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
|
||||
</UButton>
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<!-- Second row: Amount with toggle -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormField
|
||||
:label="
|
||||
stream.amountType === 'annual'
|
||||
? 'Annual amount'
|
||||
: 'Monthly amount'
|
||||
"
|
||||
required>
|
||||
<div class="flex gap-2">
|
||||
<UInput
|
||||
:value="
|
||||
stream.amountType === 'annual'
|
||||
? stream.targetAnnualAmount
|
||||
: stream.targetMonthlyAmount
|
||||
"
|
||||
type="text"
|
||||
:placeholder="
|
||||
stream.amountType === 'annual' ? '60000' : '5000'
|
||||
"
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="validateAndSaveAmount($event, stream)"
|
||||
@blur="saveStream(stream)">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500">{{ currencySymbol }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UButtonGroup size="md">
|
||||
<UButton
|
||||
:variant="
|
||||
stream.amountType === 'monthly' ? 'solid' : 'outline'
|
||||
"
|
||||
color="primary"
|
||||
@click="switchAmountType(stream, 'monthly')"
|
||||
class="text-xs">
|
||||
Monthly
|
||||
</UButton>
|
||||
<UButton
|
||||
:variant="
|
||||
stream.amountType === 'annual' ? 'solid' : 'outline'
|
||||
"
|
||||
color="primary"
|
||||
@click="switchAmountType(stream, 'annual')"
|
||||
class="text-xs">
|
||||
Annual
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||
<template v-if="stream.amountType === 'annual'">
|
||||
{{ currencySymbol
|
||||
}}{{ Math.round((stream.targetAnnualAmount || 0) / 12) }} per
|
||||
month
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ currencySymbol
|
||||
}}{{ (stream.targetMonthlyAmount || 0) * 12 }} per year
|
||||
</template>
|
||||
</p>
|
||||
<UButton
|
||||
@click="addRevenueStream"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
color="primary">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2" />
|
||||
Add your first revenue stream
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="stream in streams"
|
||||
:key="stream.id"
|
||||
class="p-6 border-2 border-black rounded-xl bg-white shadow-md">
|
||||
<!-- First row: Category and Name with delete button -->
|
||||
<div class="flex gap-4 mb-4">
|
||||
<UFormField label="Category" required class="flex-1">
|
||||
<USelect
|
||||
v-model="stream.category"
|
||||
:items="categoryOptions"
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="saveCategoryChange(stream)" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Name" required class="flex-1">
|
||||
<div class="flex gap-2">
|
||||
<USelectMenu
|
||||
v-model="stream.name"
|
||||
:items="nameOptionsByCategory[stream.category] || []"
|
||||
placeholder="Select or type a source name"
|
||||
creatable
|
||||
searchable
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="saveStream(stream)" />
|
||||
<UButton
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="error"
|
||||
@click="removeStream(stream.id)"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:opacity-90 transition-opacity',
|
||||
}">
|
||||
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
|
||||
</UButton>
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<!-- Second row: Amount with toggle -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormField :label="stream.amountType === 'annual' ? 'Annual amount' : 'Monthly amount'" required>
|
||||
<div class="flex gap-2">
|
||||
<UInput
|
||||
:value="stream.amountType === 'annual' ? stream.targetAnnualAmount : stream.targetMonthlyAmount"
|
||||
type="text"
|
||||
:placeholder="stream.amountType === 'annual' ? '60000' : '5000'"
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="validateAndSaveAmount($event, stream)"
|
||||
@blur="saveStream(stream)">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500">{{ currencySymbol }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UButtonGroup size="md">
|
||||
<UButton
|
||||
:variant="stream.amountType === 'monthly' ? 'solid' : 'outline'"
|
||||
color="primary"
|
||||
@click="switchAmountType(stream, 'monthly')"
|
||||
class="text-xs">
|
||||
Monthly
|
||||
</UButton>
|
||||
<UButton
|
||||
:variant="stream.amountType === 'annual' ? 'solid' : 'outline'"
|
||||
color="primary"
|
||||
@click="switchAmountType(stream, 'annual')"
|
||||
class="text-xs">
|
||||
Annual
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-1">
|
||||
<template v-if="stream.amountType === 'annual'">
|
||||
{{ currencySymbol }}{{ Math.round((stream.targetAnnualAmount || 0) / 12) }} per month
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ currencySymbol }}{{ (stream.targetMonthlyAmount || 0) * 12 }} per year
|
||||
</template>
|
||||
</p>
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Stream Button (when items exist) -->
|
||||
<div v-if="streams.length > 0" class="flex justify-center">
|
||||
<UButton
|
||||
@click="addRevenueStream"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
color="success"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:scale-105 transition-transform',
|
||||
leadingIcon: 'hover:rotate-90 transition-transform',
|
||||
}">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2" />
|
||||
Add another stream
|
||||
</UButton>
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Stream Button (when items exist) -->
|
||||
<div v-if="streams.length > 0" class="flex justify-center">
|
||||
<UButton
|
||||
@click="addRevenueStream"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:scale-105 transition-transform',
|
||||
leadingIcon: 'hover:rotate-90 transition-transform',
|
||||
}">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2" />
|
||||
Add another stream
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -148,23 +165,23 @@ const emit = defineEmits<{
|
|||
// Store and Currency
|
||||
const coop = useCoopBuilder();
|
||||
const { currencySymbol } = useCurrency();
|
||||
const streams = computed(() =>
|
||||
coop.streams.value.map(s => ({
|
||||
const streams = computed(() =>
|
||||
coop.streams.value.map((s) => ({
|
||||
// Map store fields to component expectations
|
||||
id: s.id,
|
||||
name: s.label,
|
||||
category: s.category || 'games',
|
||||
category: s.category || "games",
|
||||
targetMonthlyAmount: s.monthly || 0,
|
||||
targetAnnualAmount: (s.annual || (s.monthly || 0) * 12),
|
||||
amountType: s.amountType || 'monthly',
|
||||
subcategory: '',
|
||||
targetAnnualAmount: s.annual || (s.monthly || 0) * 12,
|
||||
amountType: s.amountType || "monthly",
|
||||
subcategory: "",
|
||||
targetPct: 0,
|
||||
certainty: s.certainty || 'Aspirational',
|
||||
certainty: s.certainty || "Aspirational",
|
||||
payoutDelayDays: 30,
|
||||
terms: 'Net 30',
|
||||
terms: "Net 30",
|
||||
revenueSharePct: 0,
|
||||
platformFeePct: 0,
|
||||
restrictions: 'General',
|
||||
restrictions: "General",
|
||||
seasonalityWeights: new Array(12).fill(1),
|
||||
effortHoursPerMonth: 0,
|
||||
}))
|
||||
|
|
@ -226,9 +243,10 @@ const nameOptionsByCategory: Record<string, string[]> = {
|
|||
// Computed
|
||||
const totalMonthlyAmount = computed(() =>
|
||||
streams.value.reduce((sum, s) => {
|
||||
const monthly = s.amountType === 'annual'
|
||||
? Math.round((s.targetAnnualAmount || 0) / 12)
|
||||
: (s.targetMonthlyAmount || 0);
|
||||
const monthly =
|
||||
s.amountType === "annual"
|
||||
? Math.round((s.targetAnnualAmount || 0) / 12)
|
||||
: s.targetMonthlyAmount || 0;
|
||||
return sum + monthly;
|
||||
}, 0)
|
||||
);
|
||||
|
|
@ -239,18 +257,19 @@ const debouncedSave = useDebounceFn((stream: any) => {
|
|||
|
||||
try {
|
||||
// Convert component format back to store format
|
||||
const monthly = stream.amountType === 'annual'
|
||||
? Math.round((stream.targetAnnualAmount || 0) / 12)
|
||||
: (stream.targetMonthlyAmount || 0);
|
||||
|
||||
const monthly =
|
||||
stream.amountType === "annual"
|
||||
? Math.round((stream.targetAnnualAmount || 0) / 12)
|
||||
: stream.targetMonthlyAmount || 0;
|
||||
|
||||
const streamData = {
|
||||
id: stream.id,
|
||||
label: stream.name || '',
|
||||
label: stream.name || "",
|
||||
monthly: monthly,
|
||||
annual: stream.targetAnnualAmount || monthly * 12,
|
||||
amountType: stream.amountType || 'monthly',
|
||||
category: stream.category || 'games',
|
||||
certainty: stream.certainty || 'Aspirational'
|
||||
amountType: stream.amountType || "monthly",
|
||||
category: stream.category || "games",
|
||||
certainty: stream.certainty || "Aspirational",
|
||||
};
|
||||
|
||||
coop.upsertStream(streamData);
|
||||
|
|
@ -262,10 +281,11 @@ const debouncedSave = useDebounceFn((stream: any) => {
|
|||
}, 300);
|
||||
|
||||
function saveStream(stream: any) {
|
||||
const hasValidAmount = stream.amountType === 'annual'
|
||||
? stream.targetAnnualAmount >= 0
|
||||
: stream.targetMonthlyAmount >= 0;
|
||||
|
||||
const hasValidAmount =
|
||||
stream.amountType === "annual"
|
||||
? stream.targetAnnualAmount >= 0
|
||||
: stream.targetMonthlyAmount >= 0;
|
||||
|
||||
if (stream.name && stream.category && hasValidAmount) {
|
||||
debouncedSave(stream);
|
||||
}
|
||||
|
|
@ -281,18 +301,19 @@ function saveCategoryChange(stream: any) {
|
|||
function saveStreamImmediate(stream: any) {
|
||||
try {
|
||||
// Convert component format back to store format
|
||||
const monthly = stream.amountType === 'annual'
|
||||
? Math.round((stream.targetAnnualAmount || 0) / 12)
|
||||
: (stream.targetMonthlyAmount || 0);
|
||||
|
||||
const monthly =
|
||||
stream.amountType === "annual"
|
||||
? Math.round((stream.targetAnnualAmount || 0) / 12)
|
||||
: stream.targetMonthlyAmount || 0;
|
||||
|
||||
const streamData = {
|
||||
id: stream.id,
|
||||
label: stream.name || '',
|
||||
label: stream.name || "",
|
||||
monthly: monthly,
|
||||
annual: stream.targetAnnualAmount || monthly * 12,
|
||||
amountType: stream.amountType || 'monthly',
|
||||
category: stream.category || 'games',
|
||||
certainty: stream.certainty || 'Aspirational'
|
||||
amountType: stream.amountType || "monthly",
|
||||
category: stream.category || "games",
|
||||
certainty: stream.certainty || "Aspirational",
|
||||
};
|
||||
|
||||
coop.upsertStream(streamData);
|
||||
|
|
@ -305,33 +326,35 @@ function saveStreamImmediate(stream: any) {
|
|||
function validateAndSaveAmount(value: string, stream: any) {
|
||||
const numValue = parseFloat(value.replace(/[^\d.]/g, ""));
|
||||
const validValue = isNaN(numValue) ? 0 : Math.max(0, numValue);
|
||||
|
||||
if (stream.amountType === 'annual') {
|
||||
|
||||
if (stream.amountType === "annual") {
|
||||
stream.targetAnnualAmount = validValue;
|
||||
stream.targetMonthlyAmount = Math.round(validValue / 12);
|
||||
} else {
|
||||
stream.targetMonthlyAmount = validValue;
|
||||
stream.targetAnnualAmount = validValue * 12;
|
||||
}
|
||||
|
||||
|
||||
saveStream(stream);
|
||||
}
|
||||
|
||||
// Function to switch between annual and monthly
|
||||
function switchAmountType(stream: any, type: 'annual' | 'monthly') {
|
||||
function switchAmountType(stream: any, type: "annual" | "monthly") {
|
||||
stream.amountType = type;
|
||||
|
||||
|
||||
// Recalculate values based on new type
|
||||
if (type === 'annual') {
|
||||
if (type === "annual") {
|
||||
if (!stream.targetAnnualAmount) {
|
||||
stream.targetAnnualAmount = (stream.targetMonthlyAmount || 0) * 12;
|
||||
}
|
||||
} else {
|
||||
if (!stream.targetMonthlyAmount) {
|
||||
stream.targetMonthlyAmount = Math.round((stream.targetAnnualAmount || 0) / 12);
|
||||
stream.targetMonthlyAmount = Math.round(
|
||||
(stream.targetAnnualAmount || 0) / 12
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Save immediately without debounce for instant UI update
|
||||
saveStreamImmediate(stream);
|
||||
}
|
||||
|
|
@ -344,7 +367,7 @@ function addRevenueStream() {
|
|||
annual: 0,
|
||||
amountType: "monthly",
|
||||
category: "games",
|
||||
certainty: "Aspirational"
|
||||
certainty: "Aspirational",
|
||||
};
|
||||
|
||||
coop.upsertStream(newStream);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue