| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
add2ba0713 | feat: add remaining components to dashboard | 7 months ago |
|
|
1a03253f64 |
Merge remote-tracking branch 'origin/Markeds/System-Banners' into dashboard-layout
# Conflicts: # gca-admin-gurusoft-message-dashboard/src/App.vue # gca-admin-gurusoft-message-dashboard/src/main.js # gca-admin-gurusoft-message-dashboard/src/router/index.js # package-lock.json |
7 months ago |
|
|
2da5578c43 | MarkedBanners + SystemBanners (Improved SystemBanners) | 7 months ago |
|
|
6b4611eaa3 | MarkedBanners + SystemBanners | 7 months ago |
| @ -1,9 +1,8 @@ | |||
| <script setup> | |||
| </script> | |||
| <template> | |||
| <div class="app"> | |||
| <router-view/> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| </script> | |||
| </template> | |||
| @ -1,51 +0,0 @@ | |||
| <script setup lang="ts"> | |||
| import OverlayPopup from "./OverlayPopup.vue"; | |||
| import SystemMesssages from "./SystemMesssages.vue"; | |||
| import SystemLinks from "@/components/SystemLinks.vue"; | |||
| import MarkedPopUp from "@/components/MarkedPopUp.vue"; | |||
| </script> | |||
| <style scoped> | |||
| </style> | |||
| <!--<template>--> | |||
| <!-- <div class="list-group w-25 gap-3 p-3" style="margin: auto">--> | |||
| <!-- <OverlayPopup></OverlayPopup>--> | |||
| <!-- <router-link to="/systemmessage" class="btn btn-secondary">--> | |||
| <!-- System messages--> | |||
| <!-- </router-link>--> | |||
| <!-- <router-link to="/systemmessagelist" class="btn btn-secondary">--> | |||
| <!-- System messages--> | |||
| <!-- </router-link>--> | |||
| <!-- </div>--> | |||
| <!--</template>--> | |||
| <template> | |||
| <div class="container-fluid text-center border border-3 border-dark"> | |||
| <div class="row border"> | |||
| <div class="col"> | |||
| 1 of 1 | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="col border"> | |||
| 1 of 2 | |||
| </div> | |||
| <div class="col border"> | |||
| <div class="row"> | |||
| <div class="col"> | |||
| <SystemMesssages/> | |||
| </div> | |||
| <div class="col border border-warning"> | |||
| <SystemLinks/> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <OverlayPopup/> | |||
| <MarkedPopUp/> | |||
| </template> | |||
| @ -0,0 +1,76 @@ | |||
| <template> | |||
| <div class="container mt-4"> | |||
| <h2 class="mb-3">Markeds Banner</h2> | |||
| <div class="scroll-box border rounded p-3 mx-auto"> | |||
| <div class="d-flex flex-row gap-3 scroll-content"> | |||
| <div | |||
| v-for="banner in banners" | |||
| :key="banner.id" | |||
| class="card shadow-sm" | |||
| style="width: 16rem; flex: 0 0 auto; cursor: pointer" | |||
| @click="goToPage" | |||
| > | |||
| <img | |||
| :src="banner.image" | |||
| class="card-img-top" | |||
| :alt="banner.title" | |||
| style="height: 140px; object-fit: cover" | |||
| /> | |||
| <div class="card-body"> | |||
| <h5 class="card-title">{{ banner.title }}</h5> | |||
| <p class="card-text text-muted" style="font-size: 0.85rem"> | |||
| Type: {{ banner.type }} | |||
| </p> | |||
| <a | |||
| :href="banner.url" | |||
| class="btn btn-outline-primary btn-sm" | |||
| target="_blank" | |||
| > | |||
| Gå til kampanje | |||
| </a> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| import {ref, onMounted} from 'vue' | |||
| import {useRouter} from 'vue-router' | |||
| import mockdata from '../mocks/markedsBanner.json' | |||
| const banners = ref([]) | |||
| const router = useRouter() | |||
| onMounted(() => { | |||
| banners.value = mockdata | |||
| }) | |||
| const goToPage = () => { | |||
| router.push('/markedsbanners') | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .scroll-box { | |||
| max-width: 700px; | |||
| overflow-x: auto; | |||
| white-space: nowrap; | |||
| position: fixed; | |||
| } | |||
| .scroll-content { | |||
| flex-wrap: nowrap; | |||
| } | |||
| .scroll-box::-webkit-scrollbar { | |||
| height: 8px; | |||
| } | |||
| .scroll-box::-webkit-scrollbar-thumb { | |||
| background-color: #ccc; | |||
| border-radius: 4px; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,22 @@ | |||
| <template> | |||
| <div class="container mt-4"> | |||
| <h2 class="mb-4">Alle markedsbannere</h2> | |||
| <div v-for="banner in banners" :key="banner.id" class="mb-4 border-bottom pb-3"> | |||
| <h4>{{ banner.title }}</h4> | |||
| <img :src="banner.image" alt="banner" class="img-fluid" style="max-width: 300px"/> | |||
| <p class="text-muted">Type: {{ banner.type }}</p> | |||
| <a :href="banner.url" target="_blank" class="btn btn-sm btn-outline-secondary mt-2">Besøk kampanje</a> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| import {ref, onMounted} from 'vue' | |||
| import mockdata from '../mocks/markedsBanner.json' | |||
| const banners = ref([]) | |||
| onMounted(() => { | |||
| banners.value = mockdata | |||
| }) | |||
| </script> | |||
| @ -0,0 +1,137 @@ | |||
| <template> | |||
| <div class="banner-container" v-if="activeBanners.length"> | |||
| <div class="container"> | |||
| <div class="alert-wrapper"> | |||
| <div | |||
| v-for="banner in activeBanners.slice(0,1)" | |||
| :key="banner.id" | |||
| class="alert alert-danger border-danger banner-drop d-flex justify-content-between align-items-start shadow-sm small-banner" | |||
| role="alert" | |||
| @click="goToBannerPage" | |||
| style="cursor: pointer" | |||
| > | |||
| <div class="flex-grow-1"> | |||
| <div class="d-flex flex-row justify-content-between align-items-center flex-wrap"> | |||
| <span class="me-3">{{ banner.text }}</span> | |||
| <div class="text-end"> | |||
| <a | |||
| v-if="banner.link" | |||
| :href="banner.link" | |||
| class="alert-link d-block" | |||
| target="_blank" | |||
| rel="noopener" | |||
| @click.stop | |||
| > | |||
| Mer info | |||
| </a> | |||
| <span class="badge bg-light text-dark mt-1 d-block">{{ banner.type }}</span> | |||
| </div> | |||
| </div> | |||
| <div class="text-muted mt-1" style="font-size: 0.75rem;"> | |||
| {{ formatDate(banner.updated) }} | |||
| </div> | |||
| </div> | |||
| <button | |||
| type="button" | |||
| class="btn-close ms-3" | |||
| aria-label="Close" | |||
| @click.stop="banner.dismissed = true" | |||
| ></button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| import {ref, onMounted, computed} from 'vue' | |||
| import {useRouter} from 'vue-router' | |||
| import mockdata from '../mocks/systemBanner.json' | |||
| const banners = ref([]) | |||
| const router = useRouter() | |||
| const useMockedData = import.meta.env.VITE_USE_MOCK === 'true' | |||
| const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:13000/api/systembanners' | |||
| onMounted(async () => { | |||
| try { | |||
| let data = [] | |||
| if (useMockedData) { | |||
| data = mockdata | |||
| } else { | |||
| const response = await fetch(apiUrl) | |||
| if (!response.ok) throw new Error('API-feil') | |||
| data = await response.json() | |||
| } | |||
| banners.value = data.map((b) => ({ | |||
| ...b, | |||
| dismissed: false, | |||
| })) | |||
| } catch (error) { | |||
| console.warn('API feilet, bruker mockdata:', error) | |||
| banners.value = mockdata.map((b) => ({ | |||
| ...b, | |||
| dismissed: false, | |||
| })) | |||
| } | |||
| }) | |||
| const activeBanners = computed(() => | |||
| banners.value.filter((b) => !b.dismissed) | |||
| ) | |||
| const formatDate = (iso) => { | |||
| const d = new Date(iso) | |||
| return d.toLocaleDateString('nb-NO', { | |||
| weekday: 'short', | |||
| day: '2-digit', | |||
| month: 'short', | |||
| hour: '2-digit', | |||
| minute: '2-digit', | |||
| }) | |||
| } | |||
| const goToBannerPage = (event) => { | |||
| if (event.target.closest('a') || event.target.closest('button')) { | |||
| return | |||
| } | |||
| router.push('/SystemBannerPage') | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .banner-container { | |||
| background-color: #ffffff; | |||
| padding-top: 1rem; | |||
| padding-bottom: 1rem; | |||
| border-bottom: 2px solid #dc3545; | |||
| } | |||
| .alert-wrapper { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 0.5rem; | |||
| } | |||
| .small-banner { | |||
| padding: 0.5rem 1rem; | |||
| font-size: 0.9rem; | |||
| } | |||
| .banner-drop { | |||
| animation: dropDown 0.6s ease-out forwards; | |||
| opacity: 0; | |||
| transform: translateY(-20px); | |||
| } | |||
| @keyframes dropDown { | |||
| to { | |||
| opacity: 1; | |||
| transform: translateY(0); | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,73 @@ | |||
| <template> | |||
| <div class="container mt-4"> | |||
| <h2 class="mb-4">System Banner Info</h2> | |||
| <div v-if="banners.length"> | |||
| <div | |||
| v-for="banner in banners" | |||
| :key="banner.id" | |||
| class="alert alert-danger border-danger shadow-sm mb-3" | |||
| role="alert" | |||
| > | |||
| <h5 class="fw-bold">{{ banner.title }}</h5> | |||
| <p class="text-muted" style="font-size: 0.85rem;"> | |||
| {{ formatDate(banner.updated) }} | |||
| </p> | |||
| <p>{{ banner.text }}</p> | |||
| <a | |||
| v-if="banner.link" | |||
| :href="banner.link" | |||
| class="alert-link d-block mt-2" | |||
| target="_blank" | |||
| rel="noopener" | |||
| > | |||
| Mer info | |||
| </a> | |||
| </div> | |||
| </div> | |||
| <div v-else> | |||
| <p class="text-muted">Ingen systemmeldinger tilgjengelig.</p> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| import {ref, onMounted} from 'vue' | |||
| import mockdata from '../mocks/systemBanner.json' | |||
| const banners = ref([]) | |||
| const useMockedData = import.meta.env.VITE_USE_MOCK === 'true' | |||
| const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:13000/api/systembanners' | |||
| onMounted(async () => { | |||
| try { | |||
| let data = [] | |||
| if (useMockedData) { | |||
| data = mockdata | |||
| } else { | |||
| const response = await fetch(apiUrl) | |||
| if (!response.ok) throw new Error('API-feil') | |||
| data = await response.json() | |||
| } | |||
| banners.value = data | |||
| } catch (error) { | |||
| console.warn('API feilet, bruker mockdata:', error) | |||
| banners.value = mockdata | |||
| } | |||
| }) | |||
| const formatDate = (iso) => { | |||
| const d = new Date(iso) | |||
| return d.toLocaleDateString('nb-NO', { | |||
| weekday: 'short', | |||
| day: '2-digit', | |||
| month: 'short', | |||
| hour: '2-digit', | |||
| minute: '2-digit', | |||
| }) | |||
| } | |||
| </script> | |||
| @ -0,0 +1,57 @@ | |||
| [ | |||
| { | |||
| "id": 4, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Sommersalg", | |||
| "url": "https://example.com/salg", | |||
| "image": "https://providavarmeshop.no/wp-content/uploads/2023/06/sommersalg-tekst-300x137.png", | |||
| "type": "PROMO" | |||
| }, | |||
| { | |||
| "id": 5, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Ny produktlansering", | |||
| "url": "https://example.com/nytt-produkt", | |||
| "image": "https://providavarmeshop.no/wp-content/uploads/2023/06/sommersalg-tekst-300x137.png", | |||
| "type": "PROMO" | |||
| }, | |||
| { | |||
| "id": 6, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Kundelojalitetsprogram", | |||
| "url": "https://example.com/kundelojalitet", | |||
| "image": "https://providavarmeshop.no/wp-content/uploads/2023/06/sommersalg-tekst-300x137.png", | |||
| "type": "PROMO" | |||
| }, | |||
| { | |||
| "id": 7, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Kundeservice tilgjengelig", | |||
| "url": "https://example.com/kundeservice", | |||
| "image": "https://providavarmeshop.no/wp-content/uploads/2023/06/sommersalg-tekst-300x137.png", | |||
| "type": "PROMO" | |||
| }, | |||
| { | |||
| "id": 8, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Vår nye nettbutikk er lansert!", | |||
| "url": "https://example.com/ny-nettbutikk", | |||
| "image": "https://providavarmeshop.no/wp-content/uploads/2023/06/sommersalg-tekst-300x137.png", | |||
| "type": "PROMO" | |||
| } | |||
| ] | |||
| @ -0,0 +1,35 @@ | |||
| [ | |||
| { | |||
| "id": 1, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Velkommen til systemet", | |||
| "text": "Dette er en viktig melding fra systemet.", | |||
| "link": "https://example.com/info", | |||
| "type": "WARNING" | |||
| }, | |||
| { | |||
| "id": 2, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Systemvedlikehold", | |||
| "text": "Systemet vil være utilgjengelig fra 12:00 til 14:00.", | |||
| "link": "https://example.com/maintenance", | |||
| "type": "INFO" | |||
| }, | |||
| { | |||
| "id": 3, | |||
| "createdBy": 123, | |||
| "created": "2025-06-25T10:27:34Z", | |||
| "updatedBy": 124, | |||
| "updated": "2025-06-25T10:27:34Z", | |||
| "title": "Feil i systemet", | |||
| "text": "Det har oppstått en feil i systemet. Vennligst kontakt support.", | |||
| "link": null, | |||
| "type": "ERROR" | |||
| } | |||
| ] | |||
| @ -0,0 +1,41 @@ | |||
| <script setup lang="ts"> | |||
| import OverlayPopup from "../components/OverlayPopup.vue"; | |||
| import SystemMesssages from "../components/SystemMesssages.vue"; | |||
| import SystemLinks from "../components/SystemLinks.vue"; | |||
| import MarkedsPopUp from "../components/MarkedsPopUp.vue"; | |||
| import SystemBanner from "@/components/SystemBanner.vue"; | |||
| import MarkedsBanner from "@/components/MarkedsBanner.vue"; | |||
| </script> | |||
| <style scoped> | |||
| </style> | |||
| <template> | |||
| <div class="container-fluid text-center border border-3 border-dark"> | |||
| <div class="row border"> | |||
| <div class="col"> | |||
| <SystemBanner/> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="col border"> | |||
| <MarkedsBanner/> | |||
| </div> | |||
| <div class="col border"> | |||
| <div class="row"> | |||
| <div class="col"> | |||
| <SystemMesssages/> | |||
| </div> | |||
| <div class="col border border-warning"> | |||
| <SystemLinks/> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <OverlayPopup/> | |||
| <MarkedsPopUp/> | |||
| </template> | |||
| @ -0,0 +1,5 @@ | |||
| { | |||
| "dependencies": { | |||
| "bootstrap": "^5.3.7" | |||
| } | |||
| } | |||