Merged in feature/VBV-302-durchfuehrung-ui-rebased (pull request #54)
Feature/VBV-302 durchfuehrung ui rebased Approved-by: Christian Cueni
This commit is contained in:
commit
4acccd65df
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "vue",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"baseUrl": "http://localhost:5050"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"body": "Fixtures are a great way to mock data for responses to routes",
|
||||
"email": "hello@cypress.io",
|
||||
"name": "Using fixtures to represent data"
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "../../tailwind.css";
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import { setupI18n } from "@/i18n.ts";
|
||||
import router from "@/router";
|
||||
import { mount } from "cypress/vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
// with a <reference path="./component" /> at the top of your spec.
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("mount", (component, options = {}) => {
|
||||
options.global = options.global || {};
|
||||
options.global.plugins = options.global.plugins || [];
|
||||
options.global.plugins.push(setupI18n());
|
||||
options.global.plugins.push(createPinia());
|
||||
|
||||
if (!options.router) {
|
||||
options.router = router;
|
||||
}
|
||||
|
||||
return mount(component, options);
|
||||
});
|
||||
|
||||
// Example use:
|
||||
// cy.mount(MyComponent)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,7 +15,8 @@
|
|||
"prettier:check": "prettier . --check",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "rm -rf ../server/vbv_lernwelt/static/storybook/* && storybook build -o ../server/vbv_lernwelt/static/storybook"
|
||||
"build-storybook": "rm -rf ../server/vbv_lernwelt/static/storybook/* && storybook build -o ../server/vbv_lernwelt/static/storybook",
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/tailwindcss": "^0.1.2",
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
"@sentry/vue": "^7.20.0",
|
||||
"@urql/vue": "^1.0.2",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"cypress": "^12.9.0",
|
||||
"d3": "^7.6.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"graphql": "^16.6.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<AccountMenuContent
|
||||
:course-sessions="courseSessionsStore.currentCourseSessions"
|
||||
:selected-course-session="courseSessionsStore.currentCourseSession?.id"
|
||||
:user="userStore"
|
||||
@logout="logout"
|
||||
@select-course-session="selectCourseSession"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
|
||||
const logout = () => {
|
||||
userStore.handleLogout();
|
||||
};
|
||||
const selectCourseSession = (courseSession: CourseSession) => {
|
||||
courseSessionsStore.switchCourseSession(courseSession);
|
||||
};
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const userStore = useUserStore();
|
||||
</script>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import avatar from "../../../.storybook/uk1.patrizia.huggel.jpg";
|
||||
import AccountMenuContent from "./AccountMenuContent.vue";
|
||||
const bern = {
|
||||
id: 1,
|
||||
title: "Bern 2023 a",
|
||||
};
|
||||
let selectedSession = bern;
|
||||
const courseSessions = [
|
||||
bern,
|
||||
{
|
||||
id: 2,
|
||||
title: "Zürich 2023 a",
|
||||
},
|
||||
];
|
||||
const user = {
|
||||
first_name: "Vreni",
|
||||
last_name: "Schmid",
|
||||
email: "vreni.schmid@example.com",
|
||||
avatar_url: avatar,
|
||||
loggedIn: true,
|
||||
};
|
||||
|
||||
const selectCourseSession = (session) => {
|
||||
selectedSession = session;
|
||||
console.log("session");
|
||||
};
|
||||
|
||||
describe("<AccountMenuContent />", () => {
|
||||
it("renders", () => {
|
||||
// see: https://on.cypress.io/mounting-vue
|
||||
cy.mount(AccountMenuContent, {
|
||||
props: {
|
||||
user,
|
||||
courseSessions,
|
||||
selectedCourseSession: selectedSession,
|
||||
onSelectCourseSession: selectCourseSession,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -21,25 +21,37 @@ const meta: Meta<typeof AccountMenuContent> = {
|
|||
export default meta;
|
||||
type Story = StoryObj<typeof AccountMenuContent>;
|
||||
|
||||
export const DefaultStory: Story = {
|
||||
const courseSessions = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Bern 2023 a",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Zürich 2023 a",
|
||||
},
|
||||
];
|
||||
|
||||
const loggedInUser = {
|
||||
first_name: "Vreni",
|
||||
last_name: "Schmid",
|
||||
email: "vreni.schmid@example.com",
|
||||
avatar_url: avatar,
|
||||
loggedIn: true,
|
||||
};
|
||||
|
||||
export const LoggedInUser: Story = {
|
||||
args: {
|
||||
show: true,
|
||||
courseSessions: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Bern 2023 a",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Zürich 2023 a",
|
||||
},
|
||||
],
|
||||
courseSessions,
|
||||
user: loggedInUser,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoUser: Story = {
|
||||
args: {
|
||||
courseSessions,
|
||||
user: {
|
||||
first_name: "Vreni",
|
||||
last_name: "Schmid",
|
||||
email: "vreni.schmid@example.com",
|
||||
avatar_url: avatar,
|
||||
loggedIn: true,
|
||||
loggedIn: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import CourseSessionsMenu from "@/components/header/CourseSessionsMenu.vue";
|
||||
import type { UserState } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSessions: CourseSession[];
|
||||
user: UserState | undefined;
|
||||
user: UserState;
|
||||
selectedCourseSession?: number;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(["selectCourseSession", "logout"]);
|
||||
const emit = defineEmits(["selectCourseSession", "logout"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-black">
|
||||
<div v-if="user?.loggedIn" class="border-b py-4">
|
||||
<div class="border-b py-4">
|
||||
<div class="flex justify-start">
|
||||
<div v-if="user?.avatar_url">
|
||||
<div v-if="user.avatar_url">
|
||||
<img
|
||||
class="inline-block h-20 w-20 rounded-full"
|
||||
:src="user?.avatar_url"
|
||||
:src="user.avatar_url"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<h3>{{ user?.first_name }} {{ user?.last_name }}</h3>
|
||||
<div class="text-sm text-gray-800">{{ user?.email }}</div>
|
||||
<h3>{{ user.first_name }} {{ user.last_name }}</h3>
|
||||
<div class="text-sm text-gray-800">{{ user.email }}</div>
|
||||
<div class="text-sm text-gray-800">
|
||||
<router-link class="link" to="/profile">Profil anzeigen</router-link>
|
||||
</div>
|
||||
|
|
@ -32,17 +34,14 @@ const emits = defineEmits(["selectCourseSession", "logout"]);
|
|||
</div>
|
||||
|
||||
<div v-if="props.courseSessions.length" class="border-b py-4">
|
||||
<div v-for="cs in props.courseSessions" :key="cs.id">
|
||||
<button @click="$emit('selectCourseSession', cs)">{{ cs.title }}</button>
|
||||
</div>
|
||||
<CourseSessionsMenu
|
||||
:items="courseSessions"
|
||||
:selected="selectedCourseSession"
|
||||
@select="emit('selectCourseSession', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="user?.loggedIn"
|
||||
type="button"
|
||||
class="mt-6 flex items-center"
|
||||
@click="$emit('logout')"
|
||||
>
|
||||
<button type="button" class="mt-6 flex items-center" @click="emit('logout')">
|
||||
<it-icon-logout class="inline-block" />
|
||||
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import type { Item } from "./CourseSessionsMenu.vue";
|
||||
import CourseSessionsMenu from "./CourseSessionsMenu.vue";
|
||||
|
||||
const meta: Meta<typeof CourseSessionsMenu> = {
|
||||
title: "VBV/CourseSessionsMenu",
|
||||
component: CourseSessionsMenu,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CourseSessionsMenu>;
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Bern 2021 a",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Bern 2021 b",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Bern 2022 a",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Bern 2022 b",
|
||||
},
|
||||
];
|
||||
|
||||
export const CourseSessionsMenuStory: Story = {
|
||||
args: {
|
||||
items,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<fieldset>
|
||||
<div>
|
||||
<div
|
||||
v-for="item in props.items"
|
||||
:key="item.id"
|
||||
class="mb-4 flex items-center last:mb-0"
|
||||
>
|
||||
<input
|
||||
:id="String(item.id)"
|
||||
name="coursesessions"
|
||||
type="radio"
|
||||
:value="item.id"
|
||||
:checked="selected === item.id"
|
||||
class="focus:ring-indigo-900 h-4 w-4 border-gray-300 text-blue-900"
|
||||
@change="emit('select', item)"
|
||||
/>
|
||||
<label
|
||||
:for="String(item.id)"
|
||||
class="ml-3 block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{{ item.title }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CourseSession } from "@/types";
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
items: CourseSession[];
|
||||
selected?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(["select"]);
|
||||
</script>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import router from "@/router";
|
||||
import { createPinia } from "pinia";
|
||||
import { markRaw } from "vue";
|
||||
import MainNavigationBar from "./MainNavigationBar.vue";
|
||||
|
||||
describe("<MainNavigationBar />", () => {
|
||||
it("renders", async () => {
|
||||
const pinia = createPinia();
|
||||
pinia.use(({ store }) => {
|
||||
store.router = markRaw(router);
|
||||
});
|
||||
router.push("/");
|
||||
await router.isReady();
|
||||
// see: https://on.cypress.io/mounting-vue
|
||||
cy.mount(MainNavigationBar, {
|
||||
router: router,
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
|
||||
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
|
||||
import AccountMenu from "@/components/header/AccountMenu.vue";
|
||||
import MobileMenu from "@/components/header/MobileMenu.vue";
|
||||
import NotificationPopover from "@/components/notifications/NotificationPopover.vue";
|
||||
import NotificationPopoverContent from "@/components/notifications/NotificationPopoverContent.vue";
|
||||
|
|
@ -9,20 +9,19 @@ import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
|||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useNotificationsStore } from "@/stores/notifications";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
const route = useRoute();
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const { inCockpit, inCompetenceProfile, inCourse, inLearningPath } = useRouteLookups();
|
||||
|
||||
const { t } = useI18n();
|
||||
const state = reactive({
|
||||
|
|
@ -30,37 +29,6 @@ const state = reactive({
|
|||
showMobileProfileMenu: false,
|
||||
});
|
||||
|
||||
function inCourse() {
|
||||
return route.path.startsWith("/course/");
|
||||
}
|
||||
|
||||
function inCockpit() {
|
||||
const regex = new RegExp("/course/[^/]+/cockpit");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inLearningPath() {
|
||||
const regex = new RegExp("/course/[^/]+/learn");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inCompetenceProfile() {
|
||||
const regex = new RegExp("/course/[^/]+/competence");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inMediaLibrary() {
|
||||
return route.path.startsWith("/media/");
|
||||
}
|
||||
|
||||
function logout() {
|
||||
userStore.handleLogout();
|
||||
}
|
||||
|
||||
function selectCourseSession(courseSession: CourseSession) {
|
||||
courseSessionsStore.switchCourseSession(courseSession);
|
||||
}
|
||||
|
||||
function popoverClick(event: Event) {
|
||||
if (breakpoints.smaller("lg").value) {
|
||||
event.preventDefault();
|
||||
|
|
@ -96,11 +64,7 @@ onMounted(() => {
|
|||
:show="state.showMobileProfileMenu"
|
||||
@closemodal="state.showMobileProfileMenu = false"
|
||||
>
|
||||
<AccountMenuContent
|
||||
:course-sessions="courseSessionsStore.currentCourseSessions"
|
||||
:user="userStore"
|
||||
@logout="logout"
|
||||
/>
|
||||
<AccountMenu />
|
||||
</ItFullScreenModal>
|
||||
</Teleport>
|
||||
<Transition name="nav">
|
||||
|
|
@ -129,13 +93,14 @@ onMounted(() => {
|
|||
<div
|
||||
class="ml-1 border-l border-white pl-3 pr-10 text-2xl text-white"
|
||||
>
|
||||
{{ $t("general.title") }}
|
||||
{{ t("general.title") }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden space-x-8 lg:flex">
|
||||
<!-- Navigation Links Desktop -->
|
||||
<router-link
|
||||
v-if="
|
||||
inCourse() &&
|
||||
|
|
@ -146,7 +111,7 @@ onMounted(() => {
|
|||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ $t("cockpit.title") }}
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
|
|
@ -155,7 +120,7 @@ onMounted(() => {
|
|||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ $t("general.learningPath") }}
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
|
|
@ -164,12 +129,13 @@ onMounted(() => {
|
|||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ $t("competences.title") }}
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-stretch justify-start space-x-8">
|
||||
<!-- Notification Bell & Menu -->
|
||||
<div v-if="userStore.loggedIn" class="nav-item">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
|
|
@ -217,12 +183,7 @@ onMounted(() => {
|
|||
class="absolute -right-2 top-8 z-50 w-[500px] bg-white shadow-lg"
|
||||
>
|
||||
<div class="p-4">
|
||||
<AccountMenuContent
|
||||
:course-sessions="courseSessionsStore.currentCourseSessions"
|
||||
:user="userStore"
|
||||
@logout="logout"
|
||||
@select-course-session="selectCourseSession"
|
||||
/>
|
||||
<AccountMenu />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import type { RadioItem } from "@/pages/learningPath/learningContentPage/feedback/feedback.types";
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import ItRadioGroup from "./ItRadioGroup.vue";
|
||||
|
||||
const meta: Meta<typeof ItRadioGroup> = {
|
||||
title: "VBV/RadioGroup",
|
||||
component: ItRadioGroup,
|
||||
argTypes: { "onUpdate:modelValue": { action: "updated" } },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ItRadioGroup>;
|
||||
let modelValue = "a";
|
||||
|
||||
const items: RadioItem<string>[] = [
|
||||
{
|
||||
value: "a",
|
||||
name: "A",
|
||||
},
|
||||
{
|
||||
value: "b",
|
||||
name: "B",
|
||||
},
|
||||
];
|
||||
|
||||
export const RadioGroup: Story = {
|
||||
args: {
|
||||
modelValue: modelValue,
|
||||
items: items,
|
||||
label: "Radiogroup",
|
||||
"onUpdate:modelValue": (newValue) => {
|
||||
modelValue = newValue;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import ItRadiobutton from "./ItRadiobutton.vue";
|
||||
|
||||
const meta: Meta<typeof ItRadiobutton> = {
|
||||
title: "VBV/Radiobutton",
|
||||
component: ItRadiobutton,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ItRadiobutton>;
|
||||
|
||||
export const Radiobutton: Story = {
|
||||
args: {
|
||||
item: {
|
||||
value: "Hallo",
|
||||
label: "Velo",
|
||||
subtitle: "Subtitle",
|
||||
},
|
||||
selected: false,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import type { RadioItem } from "@/components/ui/checkbox.types";
|
||||
import log from "loglevel";
|
||||
|
||||
const props = defineProps<{
|
||||
item: RadioItem<any>;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["toggle"]);
|
||||
const toggle = () => {
|
||||
emit("toggle");
|
||||
};
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
log.debug("keydown", e.type, e.key);
|
||||
if (e.key === " " && !props.disabled) {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
};
|
||||
const input = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
log.debug("input", e.type, target.checked, target.value);
|
||||
emit("toggle");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'opacity-50': disabled,
|
||||
'cursor-not-allowed': disabled,
|
||||
}"
|
||||
class="inline-flex cursor-pointer"
|
||||
>
|
||||
<label
|
||||
class="cy-checkbox cy-checkbox-checked block flex h-8 items-center bg-contain bg-no-repeat pl-8 disabled:opacity-50"
|
||||
:class="
|
||||
item.checked
|
||||
? 'bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]'
|
||||
: 'bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]'
|
||||
"
|
||||
tabindex="0"
|
||||
@keydown.stop="keydown"
|
||||
>
|
||||
<input
|
||||
ref="checkbox"
|
||||
:checked="item.checked"
|
||||
:value="item.value"
|
||||
:disabled="disabled"
|
||||
:data-cy="`it-checkbox-${item.value}`"
|
||||
class="sr-only"
|
||||
type="checkbox"
|
||||
@keydown="keydown"
|
||||
@input="input"
|
||||
/>
|
||||
<div class="ml-4 flex-col">
|
||||
<div v-if="item.label">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div v-if="item.subtitle" class="text-gray-900">
|
||||
{{ item.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
export interface CheckboxItem<T> {
|
||||
export interface BaseItem<T> {
|
||||
value: T;
|
||||
label?: string;
|
||||
checked: boolean;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export interface CheckboxItem<T> extends BaseItem<T> {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
export interface RadioItem<T> extends BaseItem<T> {
|
||||
selected: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { useRoute } from "vue-router";
|
||||
|
||||
export function useRouteLookups() {
|
||||
const route = useRoute();
|
||||
|
||||
function inCourse() {
|
||||
return route.path.startsWith("/course/");
|
||||
}
|
||||
|
||||
function inCockpit() {
|
||||
const regex = new RegExp("/course/[^/]+/cockpit");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inLearningPath() {
|
||||
const regex = new RegExp("/course/[^/]+/learn");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inCompetenceProfile() {
|
||||
const regex = new RegExp("/course/[^/]+/competence");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inMediaLibrary() {
|
||||
return route.path.startsWith("/media/");
|
||||
}
|
||||
|
||||
return { inMediaLibrary, inCockpit, inLearningPath, inCompetenceProfile, inCourse };
|
||||
}
|
||||
Loading…
Reference in New Issue