start component tree + implem navbar
This commit is contained in:
Generated
+1617
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -8,9 +8,13 @@
|
|||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/vue": "^1.7.23",
|
||||||
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@supabase/supabase-js": "^2.112.3",
|
"@supabase/supabase-js": "^2.112.3",
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"pinia": "^4.0.3",
|
"pinia": "^4.0.3",
|
||||||
@@ -22,16 +26,20 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^24.13.3",
|
"@types/node": "^24.13.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.8",
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
|
"@vitest/ui": "^4.1.11",
|
||||||
"@vue/eslint-config-typescript": "^14.9.0",
|
"@vue/eslint-config-typescript": "^14.9.0",
|
||||||
|
"@vue/test-utils": "^2.4.11",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"eslint-plugin-vue": "^10.10.0",
|
"eslint-plugin-vue": "^10.10.0",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.67.0",
|
"typescript-eslint": "^8.67.0",
|
||||||
"vite": "^8.2.0",
|
"vite": "^8.2.0",
|
||||||
|
"vitest": "^4.1.11",
|
||||||
"vue-tsc": "^3.3.8"
|
"vue-tsc": "^3.3.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import EntryItem from './EntryItem.vue'
|
||||||
|
|
||||||
|
describe('EntryItem', () => {
|
||||||
|
it('renders the label and formatted amount', () => {
|
||||||
|
const wrapper = mount(EntryItem, {
|
||||||
|
props: { label: 'Rent', amount: 850 },
|
||||||
|
})
|
||||||
|
expect(wrapper.text()).toContain('Rent')
|
||||||
|
expect(wrapper.text()).toContain('850')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits "remove" with the entry id when delete is clicked', async () => {
|
||||||
|
const wrapper = mount(EntryItem, {
|
||||||
|
props: { id: 'random_id', label: 'Rent', amount: 850 },
|
||||||
|
})
|
||||||
|
await wrapper.find('[data-test="delete-btn"]').trigger('click')
|
||||||
|
expect(wrapper.emitted('remove')).toEqual([['random_id']])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
const wrapper = mount(EntryItem, {
|
||||||
|
props: { label: 'Rent', amount: 850 },
|
||||||
|
})
|
||||||
|
expect(wrapper.html()).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<template></template>
|
||||||
|
<script setup lang="ts"></script>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<Disclosure as="nav" class="rounded bg-tints-0 dark:bg-shades-100" v-slot="{ open }">
|
||||||
|
<div class="flex h-16 items-center justify-between px-4">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<span class="text-lg font-semibold text-shades-200 dark:text-shades-900">Budget</span>
|
||||||
|
<div class="hidden sm:flex sm:gap-2">
|
||||||
|
<a
|
||||||
|
v-for="item in navigation"
|
||||||
|
:key="item.name"
|
||||||
|
:href="item.href"
|
||||||
|
:class="[
|
||||||
|
item.current
|
||||||
|
? 'bg-shades-500 text-tints-0 dark:bg-tints-1000 dark:text-shades-100'
|
||||||
|
: 'text-shades-300 hover:bg-shades-800 dark:text-shades-700 dark:hover:bg-shades-300/20',
|
||||||
|
'rounded-md px-3 py-2 text-sm font-medium',
|
||||||
|
]"
|
||||||
|
:aria-current="item.current ? 'page' : undefined"
|
||||||
|
>{{ item.name }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md p-2 text-shades-300 hover:bg-shades-800 dark:text-shades-700 dark:hover:bg-shades-300/20"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Toggle dark mode</span>
|
||||||
|
<SunIcon v-if="isDark" class="size-5" aria-hidden="true" />
|
||||||
|
<MoonIcon v-else class="size-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<DisclosureButton
|
||||||
|
class="rounded-md p-2 text-shades-300 hover:bg-shades-800 sm:hidden dark:text-shades-700 dark:hover:bg-shades-300/20"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<Bars3Icon v-if="!open" class="size-6" aria-hidden="true" />
|
||||||
|
<XMarkIcon v-else class="size-6" aria-hidden="true" />
|
||||||
|
</DisclosureButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DisclosurePanel class="space-y-1 px-4 pb-3 sm:hidden">
|
||||||
|
<DisclosureButton
|
||||||
|
v-for="item in navigation"
|
||||||
|
:key="item.name"
|
||||||
|
as="a"
|
||||||
|
:href="item.href"
|
||||||
|
:class="[
|
||||||
|
item.current
|
||||||
|
? 'bg-shades-500 text-tints-0 dark:bg-tints-1000 dark:text-shades-100'
|
||||||
|
: 'text-shades-300 hover:bg-shades-800 dark:text-shades-700 dark:hover:bg-shades-300/20',
|
||||||
|
'block rounded-md px-3 py-2 text-base font-medium',
|
||||||
|
]"
|
||||||
|
:aria-current="item.current ? 'page' : undefined"
|
||||||
|
>{{ item.name }}</DisclosureButton
|
||||||
|
>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||||
|
import { Bars3Icon, MoonIcon, SunIcon, XMarkIcon } from '@heroicons/vue/24/outline'
|
||||||
|
|
||||||
|
const { isDark, toggle } = useDarkMode()
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', href: '#', current: true },
|
||||||
|
{ name: 'History', href: '#', current: false },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-16 flex items-center justify-start bg-tints-0 dark:bg-shades-100">
|
||||||
|
<span>Amount</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<template>Spend Columns</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<template>Spend Input</template>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isDark = ref(false)
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
document.documentElement.classList.toggle('dark', isDark.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const stored = localStorage.getItem('theme')
|
||||||
|
isDark.value = stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
export function useDarkMode() {
|
||||||
|
return { isDark, toggle }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Navbar from '@/components/Navbar/Navbar.vue'
|
||||||
|
import RemainingAmount from '@/components/RemainingAmount/RemainingAmount.vue'
|
||||||
|
import SpendColumns from '@/components/SpendColumns/SpendColumns.vue'
|
||||||
|
import SpendInput from '@/components/SpendInput/SpendInput.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container flex flex-col space-x-1 space-y-2 p-2">
|
||||||
|
<div class="w-full rounded"><Navbar /></div>
|
||||||
|
<div class="h-12 w-full rounded"><RemainingAmount /></div>
|
||||||
|
<div class="h-128 w-full rounded"><SpendColumns /></div>
|
||||||
|
<div class="h-16 w-full rounded"><SpendInput /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<template></template>
|
||||||
|
|||||||
+24
-1
@@ -1 +1,24 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-shades-900: #f8ead3;
|
||||||
|
--color-shades-800: #f2d6a6;
|
||||||
|
--color-shades-700: #edc278;
|
||||||
|
--color-shades-600: #eaaf48;
|
||||||
|
--color-shades-500: #e89b17;
|
||||||
|
--color-shades-400: #bc7d10;
|
||||||
|
--color-shades-300: #8f5e0a;
|
||||||
|
--color-shades-200: #613f05;
|
||||||
|
--color-shades-100: #312002;
|
||||||
|
--color-tints-1000: #f59f0a;
|
||||||
|
--color-tints-880: #f2a92c;
|
||||||
|
--color-tints-750: #f0b248;
|
||||||
|
--color-tints-630: #efbd67;
|
||||||
|
--color-tints-500: #efc885;
|
||||||
|
--color-tints-380: #f0d29e;
|
||||||
|
--color-tints-250: #f3deba;
|
||||||
|
--color-tints-130: #f5e8d1;
|
||||||
|
--color-tints-0: #faf4ea;
|
||||||
|
}
|
||||||
+5
-1
@@ -1,11 +1,15 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), tailwindcss()],
|
plugins: [vue(), tailwindcss()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
|||||||
Reference in New Issue
Block a user