| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
2da5578c43 | MarkedBanners + SystemBanners (Improved SystemBanners) | 7 months ago |
|
|
6b4611eaa3 | MarkedBanners + SystemBanners | 7 months ago |
| @ -1,47 +1,8 @@ | |||||
| <script setup> | |||||
| import HelloWorld from './components/HelloWorld.vue' | |||||
| import TheWelcome from './components/TheWelcome.vue' | |||||
| </script> | |||||
| <template> | <template> | ||||
| <header> | |||||
| <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" /> | |||||
| <div class="wrapper"> | |||||
| <HelloWorld msg="You did it!" /> | |||||
| </div> | |||||
| </header> | |||||
| <div class="app"> | |||||
| <SystemBanner /> | |||||
| <main> | |||||
| <TheWelcome /> | |||||
| </main> | |||||
| <router-view /> | |||||
| </div> | |||||
| </template> | </template> | ||||
| <style scoped> | |||||
| header { | |||||
| line-height: 1.5; | |||||
| } | |||||
| .logo { | |||||
| display: block; | |||||
| margin: 0 auto 2rem; | |||||
| } | |||||
| @media (min-width: 1024px) { | |||||
| header { | |||||
| display: flex; | |||||
| place-items: center; | |||||
| padding-right: calc(var(--section-gap) / 2); | |||||
| } | |||||
| .logo { | |||||
| margin: 0 2rem 0 0; | |||||
| } | |||||
| header .wrapper { | |||||
| display: flex; | |||||
| place-items: flex-start; | |||||
| flex-wrap: wrap; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,18 @@ | |||||
| <template> | |||||
| <div> | |||||
| <h3>{{ banner.title }}</h3> | |||||
| <p>{{ banner.text }}</p> | |||||
| <a :href="banner.link" target="_blank">{{ banner.link }}</a> | |||||
| </div> | |||||
| </template> | |||||
| <script setup> | |||||
| import { computed } from 'vue' | |||||
| import mockData from '../mocks/mockdata.json' | |||||
| const props = defineProps(['elementId']) | |||||
| const banner = computed(() => mockData.find(item => item.id.toString() === props.elementId)) | |||||
| </script> | |||||
| @ -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" | |||||
| @click.stop | |||||
| > | |||||
| 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" | |||||
| :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/mockdata.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/mockdata.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> | |||||
| @ -1,6 +1,12 @@ | |||||
| import './assets/main.css' | |||||
| import { createApp } from 'vue' | import { createApp } from 'vue' | ||||
| import App from './App.vue' | import App from './App.vue' | ||||
| import router from './router' | |||||
| import 'bootstrap/dist/css/bootstrap.min.css' | |||||
| import './assets/base.css' | |||||
| const app = createApp(App) | |||||
| createApp(App).mount('#app') | |||||
| app.use(router) | |||||
| app.mount('#app') | |||||
| @ -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,35 @@ | |||||
| import { createRouter, createWebHistory } from 'vue-router' | |||||
| import SystemBannerPage from '../components/SystemBannerPage.vue' | |||||
| import SystemBanner from '../components/SystemBanner.vue' | |||||
| import MarkedsBannerPage from '../components/MarkedsBannerPage.vue' | |||||
| import MarkedsBanner from '../components/MarkedsBanner.vue' | |||||
| const routes = [ | |||||
| { | |||||
| path: '/', | |||||
| name: 'Home', | |||||
| component: SystemBanner | |||||
| }, | |||||
| { | |||||
| path: '/SystemBannerPage', | |||||
| name: 'BannerPage', | |||||
| component: SystemBannerPage | |||||
| }, | |||||
| { | |||||
| path: '/markedsbanners', | |||||
| name: 'MarkedsPage', | |||||
| component: MarkedsBannerPage | |||||
| }, | |||||
| { | |||||
| path: '/markedsbanner', | |||||
| name: 'MarkedsStart', | |||||
| component: MarkedsBanner | |||||
| } | |||||
| ] | |||||
| const router = createRouter({ | |||||
| history: createWebHistory(), | |||||
| routes | |||||
| }) | |||||
| export default router | |||||
| @ -0,0 +1,42 @@ | |||||
| { | |||||
| "name": "gca_admin", | |||||
| "lockfileVersion": 3, | |||||
| "requires": true, | |||||
| "packages": { | |||||
| "": { | |||||
| "dependencies": { | |||||
| "bootstrap": "^5.3.7" | |||||
| } | |||||
| }, | |||||
| "node_modules/@popperjs/core": { | |||||
| "version": "2.11.8", | |||||
| "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", | |||||
| "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", | |||||
| "license": "MIT", | |||||
| "peer": true, | |||||
| "funding": { | |||||
| "type": "opencollective", | |||||
| "url": "https://opencollective.com/popperjs" | |||||
| } | |||||
| }, | |||||
| "node_modules/bootstrap": { | |||||
| "version": "5.3.7", | |||||
| "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", | |||||
| "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", | |||||
| "funding": [ | |||||
| { | |||||
| "type": "github", | |||||
| "url": "https://github.com/sponsors/twbs" | |||||
| }, | |||||
| { | |||||
| "type": "opencollective", | |||||
| "url": "https://opencollective.com/bootstrap" | |||||
| } | |||||
| ], | |||||
| "license": "MIT", | |||||
| "peerDependencies": { | |||||
| "@popperjs/core": "^2.11.8" | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,5 @@ | |||||
| { | |||||
| "dependencies": { | |||||
| "bootstrap": "^5.3.7" | |||||
| } | |||||
| } | |||||