Skip to content

Commit b6eb560

Browse files
luiggibcnLuiggi13Christian Llansola
authored
Enhancement/table store (#8)
* Logic into table store * Move edit icon to the bottom * Tables to store and refactor --------- Co-authored-by: Christian Llansola <christian.llansola@gmail.com> Co-authored-by: Christian Llansola <christian.llansola@mail.schwarz>
1 parent 52d1f34 commit b6eb560

14 files changed

Lines changed: 905 additions & 579 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<template>
2+
<!-- Header Sticky -->
3+
<header class="sticky top-0 z-30 bg-white border-b border-gray-200 transition-shadow"
4+
:class="{ 'shadow-md': isScrolled }">
5+
<div class="flex items-center justify-between px-6 py-3 h-16">
6+
<!-- Títol pàgina -->
7+
<h1 class="text-lg font-semibold text-gray-900">{{ t('sidebar.menu') }}</h1>
8+
9+
<!-- Right Section: Cart + Language + User -->
10+
<div class="flex items-center gap-3">
11+
<!-- Cart icon -->
12+
<div class="relative text-black">
13+
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
14+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
15+
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
16+
</svg>
17+
<span v-if="cart.itemCount > 0"
18+
class="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg animate-bounce">
19+
{{ cart.itemCount > 99 ? '99+' : cart.itemCount }}
20+
</span>
21+
</div>
22+
23+
<!-- Language Selector -->
24+
<language-side-bar />
25+
26+
<!-- User Profile -->
27+
<div class="flex items-center gap-2">
28+
<div class="w-9 h-9 rounded-full bg-gray-300 overflow-hidden cursor-pointer">
29+
<img src="https://placehold.co/40" alt="Sania" class="w-full h-full object-cover" />
30+
</div>
31+
<div class="hidden md:block">
32+
<p class="text-sm font-medium text-gray-900 leading-tight">Sania</p>
33+
<p class="text-xs text-gray-500">Webber</p>
34+
</div>
35+
</div>
36+
</div>
37+
</div>
38+
</header>
39+
</template>
40+
<script setup lang="ts">
41+
import { useCartStore } from '@/stores/cart.store';
42+
import { onMounted, onUnmounted, ref } from 'vue';
43+
import { useI18n } from 'vue-i18n';
44+
import LanguageSideBar from './switchers/LanguageSideBar.vue';
45+
46+
const cart = useCartStore()
47+
const { t } = useI18n()
48+
const isScrolled = ref(false)
49+
50+
const handleScroll = () => {
51+
isScrolled.value = window.scrollY > 10
52+
}
53+
54+
onMounted(() => {
55+
window.addEventListener('scroll', handleScroll)
56+
})
57+
58+
onUnmounted(() => {
59+
window.removeEventListener('scroll', handleScroll)
60+
})
61+
</script>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<template>
2+
<aside
3+
:class="[
4+
'flex flex-col bg-white border-r border-gray-200 shrink-0 transition-all duration-300 ease-in-out',
5+
sidebarOpen ? 'md:w-60 w-full' : 'w-16'
6+
]"
7+
class="sticky top-0 h-screen z-40 overflow-hidden"
8+
>
9+
<!-- Logo + toggle -->
10+
<div class="flex items-center h-16 px-3 border-b border-gray-100 shrink-0"
11+
:class="sidebarOpen ? 'justify-between' : 'justify-center'">
12+
<div v-if="sidebarOpen" class="flex items-center gap-2 overflow-hidden">
13+
<div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center shrink-0">
14+
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
16+
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
17+
</svg>
18+
</div>
19+
<span class="font-bold text-gray-900 text-sm whitespace-nowrap">Payment4You</span>
20+
</div>
21+
<button @click="sidebarOpen = !sidebarOpen"
22+
class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-500 transition-colors cursor-pointer shrink-0">
23+
<!-- Icona col·lapsar / expandir -->
24+
<svg v-if="sidebarOpen" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
25+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7M18 19l-7-7 7-7" />
26+
</svg>
27+
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M6 5l7 7-7 7" />
29+
</svg>
30+
</button>
31+
</div>
32+
33+
<!-- Nav items -->
34+
<nav class="flex-1 py-4 space-y-1 px-2 overflow-hidden">
35+
<button v-for="item in navItems" :key="item.key"
36+
@click="changePage(item.url)"
37+
:class="[
38+
'w-full flex items-center rounded-xl transition-all cursor-pointer group',
39+
sidebarOpen ? 'px-3 py-2.5 gap-3' : 'px-3 py-2.5',
40+
item.active
41+
? 'bg-orange-50 text-orange-500'
42+
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-900'
43+
]">
44+
<span class="shrink-0" v-html="item.icon"></span>
45+
<span v-if="sidebarOpen" class="text-sm font-medium whitespace-nowrap">{{ t(item.labelKey) }}</span>
46+
</button>
47+
</nav>
48+
49+
<!-- Sign out -->
50+
<div class="px-2 pb-4 shrink-0">
51+
<button @click="userSignOut"
52+
:class="[
53+
'w-full flex items-center rounded-xl transition-all cursor-pointer text-gray-500 hover:bg-red-50 hover:text-red-500',
54+
sidebarOpen ? 'px-3 py-2.5 gap-3' : 'px-0 py-2.5 justify-center'
55+
]">
56+
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
58+
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
59+
</svg>
60+
<span v-if="sidebarOpen" class="text-sm font-medium whitespace-nowrap">{{ t('sidebar.signOut') }}</span>
61+
</button>
62+
</div>
63+
</aside>
64+
</template>
65+
<script setup lang="ts">
66+
import { useIsMobile } from '@/composables/isMobile.composable';
67+
import { useAuthStore } from '@/stores/auth.store';
68+
import { redirectTo } from '@/utils';
69+
import { onMounted, ref } from 'vue';
70+
import { useI18n } from 'vue-i18n';
71+
72+
const authStore = useAuthStore()
73+
const { isMobile } = useIsMobile()
74+
const { t } = useI18n()
75+
const sidebarOpen = ref(true)
76+
77+
onMounted(() => {
78+
if(isMobile) sidebarOpen.value = false
79+
})
80+
//TODO: REFACTOR COLORS IN NAVITEMS
81+
const navItems = [
82+
{
83+
key: 'dashboard',
84+
labelKey: 'sidebar.dashboard',
85+
active: false,
86+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>`,
87+
url: ''
88+
},
89+
{
90+
key: 'menu',
91+
labelKey: 'sidebar.menu',
92+
active: true,
93+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>`,
94+
url: ''
95+
},
96+
{
97+
key: 'stock',
98+
labelKey: 'sidebar.stock',
99+
active: false,
100+
icon: `<svg class="w-5 h-5" fill="none" stroke="#df0000" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg>`,
101+
url: 'tpv/products'
102+
},
103+
{
104+
key: 'table',
105+
labelKey: 'sidebar.table',
106+
active: false,
107+
icon: `<svg class="w-5 h-5" fill="none" stroke="#df0000" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>`,
108+
url: 'tpv'
109+
},
110+
{
111+
key: 'history',
112+
labelKey: 'sidebar.history',
113+
active: false,
114+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
115+
url: ''
116+
},
117+
{
118+
key: 'settings',
119+
labelKey: 'sidebar.settings',
120+
active: false,
121+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>`,
122+
url: ''
123+
},
124+
]
125+
126+
const userSignOut = async (): Promise<void> => {
127+
try {
128+
await authStore.signOut()
129+
} catch (_) { }
130+
}
131+
const changePage = (url: string) => {
132+
if (url.trim() !== '') {
133+
sidebarOpen.value = false
134+
redirectTo(`/${url}`)
135+
}
136+
}
137+
</script>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<template>
2+
<div class="relative" ref="languageDropdownRef">
3+
<button @click="toggleLanguageDropdown"
4+
class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer">
5+
<span class="text-xl">{{ selectedLanguage.flag }}</span>
6+
<span class="hidden md:inline text-sm font-medium text-gray-700">{{ selectedLanguage.code }}</span>
7+
<svg class="w-4 h-4 text-gray-500 transition-transform" :class="{ 'rotate-180': isLanguageDropdownOpen }"
8+
fill="none" stroke="currentColor" viewBox="0 0 24 24">
9+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
10+
</svg>
11+
</button>
12+
<transition enter-active-class="transition ease-out duration-100"
13+
enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100"
14+
leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100"
15+
leave-to-class="transform opacity-0 scale-95">
16+
<div v-show="isLanguageDropdownOpen"
17+
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
18+
<button v-for="language in languages" :key="language.code" @click="selectLanguage(language)"
19+
:disabled="!language.available"
20+
class="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors cursor-pointer text-left"
21+
:class="{ 'bg-emerald-50': selectedLanguage.code === language.code, 'hidden': !language.available }">
22+
<span class="text-xl">{{ language.flag }}</span>
23+
<span class="text-sm font-medium text-gray-700">{{ language.name }}</span>
24+
<svg v-if="selectedLanguage.code === language.code" class="w-4 h-4 text-emerald-600 ml-auto"
25+
fill="currentColor" viewBox="0 0 20 20">
26+
<path fill-rule="evenodd"
27+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
28+
clip-rule="evenodd" />
29+
</svg>
30+
</button>
31+
</div>
32+
</transition>
33+
</div>
34+
</template>
35+
<script setup lang="ts">
36+
import { onMounted, onUnmounted, ref } from 'vue';
37+
import { mainLanguages, type Language, type Locale } from '@/locales'
38+
import { setLocale } from '@/plugins/i18n';
39+
40+
const languages = ref<Language[]>(mainLanguages)
41+
42+
const selectedLanguage = ref<Language>(languages.value[1]) // Español por defecto
43+
const isLanguageDropdownOpen = ref(false)
44+
const languageDropdownRef = ref<HTMLElement | null>(null)
45+
46+
const toggleLanguageDropdown = () => {
47+
isLanguageDropdownOpen.value = !isLanguageDropdownOpen.value
48+
}
49+
50+
const changeLanguage = (newLocale: Language) => {
51+
setLocale(newLocale.code as Locale)
52+
}
53+
54+
const selectLanguage = (language: Language) => {
55+
if (!language.available) return
56+
selectedLanguage.value = language
57+
isLanguageDropdownOpen.value = false
58+
changeLanguage(language)
59+
}
60+
61+
const handleClickOutside = (event: MouseEvent) => {
62+
if (languageDropdownRef.value && !languageDropdownRef.value.contains(event.target as Node)) {
63+
isLanguageDropdownOpen.value = false
64+
}
65+
}
66+
67+
onMounted(() => {
68+
document.addEventListener('click', handleClickOutside)
69+
})
70+
71+
onUnmounted(() => {
72+
document.removeEventListener('click', handleClickOutside)
73+
})
74+
</script>

0 commit comments

Comments
 (0)