4 Commits

Author SHA1 Message Date
  ken add2ba0713 feat: add remaining components to dashboard 7 months ago
  ken 1a03253f64 Merge remote-tracking branch 'origin/Markeds/System-Banners' into dashboard-layout 7 months ago
  alan 2da5578c43 MarkedBanners + SystemBanners (Improved SystemBanners) 7 months ago
  alan 6b4611eaa3 MarkedBanners + SystemBanners 7 months ago
16 changed files with 498 additions and 61 deletions
Unified View
  1. +4
    -5
      gca-admin-gurusoft-message-dashboard/src/App.vue
  2. +0
    -51
      gca-admin-gurusoft-message-dashboard/src/components/Dashboard.vue
  3. +76
    -0
      gca-admin-gurusoft-message-dashboard/src/components/MarkedsBanner.vue
  4. +22
    -0
      gca-admin-gurusoft-message-dashboard/src/components/MarkedsBannerPage.vue
  5. +0
    -0
      gca-admin-gurusoft-message-dashboard/src/components/MarkedsPopUp.vue
  6. +137
    -0
      gca-admin-gurusoft-message-dashboard/src/components/SystemBanner.vue
  7. +73
    -0
      gca-admin-gurusoft-message-dashboard/src/components/SystemBannerPage.vue
  8. +1
    -1
      gca-admin-gurusoft-message-dashboard/src/components/SystemMessagesPage.vue
  9. +57
    -0
      gca-admin-gurusoft-message-dashboard/src/mocks/markedsBanner.json
  10. +0
    -0
      gca-admin-gurusoft-message-dashboard/src/mocks/mockdata.json
  11. +35
    -0
      gca-admin-gurusoft-message-dashboard/src/mocks/systemBanner.json
  12. +10
    -2
      gca-admin-gurusoft-message-dashboard/src/router/index.js
  13. +41
    -0
      gca-admin-gurusoft-message-dashboard/src/views/Dashboard.vue
  14. +0
    -1
      gca-admin-gurusoft-message-dashboard/vite.config.js
  15. +37
    -1
      package-lock.json
  16. +5
    -0
      package.json

+ 4
- 5
gca-admin-gurusoft-message-dashboard/src/App.vue View File

@ -1,9 +1,8 @@
<script setup>
</script>
<template> <template>
<div class="app"> <div class="app">
<router-view/> <router-view/>
</div> </div>
</template>
<script setup>
</script>
</template>

+ 0
- 51
gca-admin-gurusoft-message-dashboard/src/components/Dashboard.vue View File

@ -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>

+ 76
- 0
gca-admin-gurusoft-message-dashboard/src/components/MarkedsBanner.vue View File

@ -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"
>
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>

+ 22
- 0
gca-admin-gurusoft-message-dashboard/src/components/MarkedsBannerPage.vue View File

@ -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>

gca-admin-gurusoft-message-dashboard/src/components/MarkedPopUp.vue → gca-admin-gurusoft-message-dashboard/src/components/MarkedsPopUp.vue View File


+ 137
- 0
gca-admin-gurusoft-message-dashboard/src/components/SystemBanner.vue View File

@ -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>

+ 73
- 0
gca-admin-gurusoft-message-dashboard/src/components/SystemBannerPage.vue View File

@ -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>

+ 1
- 1
gca-admin-gurusoft-message-dashboard/src/components/SystemMessagesPage.vue View File

@ -1,7 +1,7 @@
<template> <template>
<div class="container-sm py-4"> <div class="container-sm py-4">
<div class="mb-3"> <div class="mb-3">
<router-link to="/dashboard" class="btn btn-sm btn-outline-primary">
<router-link to="/" class="btn btn-sm btn-outline-primary">
Tilbake Tilbake
</router-link> </router-link>
</div> </div>

+ 57
- 0
gca-admin-gurusoft-message-dashboard/src/mocks/markedsBanner.json View File

@ -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
gca-admin-gurusoft-message-dashboard/src/mocks/mockdata.json View File


+ 35
- 0
gca-admin-gurusoft-message-dashboard/src/mocks/systemBanner.json View File

@ -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"
}
]

+ 10
- 2
gca-admin-gurusoft-message-dashboard/src/router/index.js View File

@ -1,14 +1,22 @@
import {createRouter, createWebHistory} from 'vue-router'; import {createRouter, createWebHistory} from 'vue-router';
import SystemMessagesPage from '../components/SystemMessagesPage.vue'; import SystemMessagesPage from '../components/SystemMessagesPage.vue';
import SystemMessages from '../components/SystemMesssages.vue'; import SystemMessages from '../components/SystemMesssages.vue';
import Dashboard from '../components/Dashboard.vue';
import Dashboard from '../views/Dashboard.vue';
import SystemLinks from '../components/SystemLinks.vue' import SystemLinks from '../components/SystemLinks.vue'
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 = [ const routes = [
{path: '/systemmessage', name: 'SystemMessage', component: SystemMessages}, {path: '/systemmessage', name: 'SystemMessage', component: SystemMessages},
{path: '/systemmessagelist', name: 'SystemMessageList', component: SystemMessagesPage}, {path: '/systemmessagelist', name: 'SystemMessageList', component: SystemMessagesPage},
{path: '/systemlinks', name: 'SystemLinks', component: SystemLinks}, {path: '/systemlinks', name: 'SystemLinks', component: SystemLinks},
{path: '/', name: 'Dashboard', component: Dashboard}
{path: '/', name: 'Dashboard', component: Dashboard},
{path: '/systembanner', 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({ const router = createRouter({

+ 41
- 0
gca-admin-gurusoft-message-dashboard/src/views/Dashboard.vue View File

@ -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
- 1
gca-admin-gurusoft-message-dashboard/vite.config.js View File

@ -4,7 +4,6 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),

+ 37
- 1
package-lock.json View File

@ -2,5 +2,41 @@
"name": "sommer2025", "name": "sommer2025",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": {}
"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"
}
}
}
} }

+ 5
- 0
package.json View File

@ -0,0 +1,5 @@
{
"dependencies": {
"bootstrap": "^5.3.7"
}
}

Loading…
Cancel
Save