Add Category Pills to Module

This commit is contained in:
Lorenz Padberg 2023-08-15 15:32:07 +02:00
parent 475afd03ed
commit 6d312da0ae
9 changed files with 240 additions and 177 deletions

View File

@ -4,12 +4,23 @@
class="module" class="module"
v-if="module.id" v-if="module.id"
> >
<h2 <div class="module__header">
class="module__meta-title" <div>
id="meta-title" <h2
> class="module__meta-title"
{{ module.metaTitle }} id="meta-title"
</h2> >
{{ module.metaTitle }}
</h2>
</div>
<div>
<pill :text="module.category?.name"></pill>
<pill :text="module.categoryType?.name"></pill>
</div>
</div>
<h1 <h1
class="module__title" class="module__title"
data-cy="module-title" data-cy="module-title"
@ -85,116 +96,127 @@
</template> </template>
<script> <script>
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue'; import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
import Chapter from '@/components/Chapter.vue'; import Chapter from '@/components/Chapter.vue';
import BookmarkActions from '@/components/notes/BookmarkActions.vue'; import BookmarkActions from '@/components/notes/BookmarkActions.vue';
import Pill from "@/components/ui/Pill.vue";
export default { export default {
props: { props: {
module: { module: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
},
}, },
},
components: { components: {
BookmarkActions, Pill,
ObjectiveGroups, BookmarkActions,
Chapter, ObjectiveGroups,
}, Chapter,
},
computed: { computed: {
languageCommunicationObjectiveGroups() { languageCommunicationObjectiveGroups() {
return this.module.objectiveGroups return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'language_communication') ? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'language_communication')
: []; : [];
},
societyObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'society')
: [];
},
interdisciplinaryObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'interdisciplinary')
: [];
},
note() {
if (!(this.module && this.module.bookmark)) {
return;
}
return this.module.bookmark.note;
},
}, },
societyObjectiveGroups() { };
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'society')
: [];
},
interdisciplinaryObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'interdisciplinary')
: [];
},
note() {
if (!(this.module && this.module.bookmark)) {
return;
}
return this.module.bookmark.note;
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import 'styles/helpers'; @import 'styles/helpers';
.module { .module {
display: flex; display: flex;
justify-self: center; justify-self: center;
max-width: 100vw; max-width: 100vw;
padding: $large-spacing 0; padding: $large-spacing 0;
@include desktop { @include desktop {
width: 800px; width: 800px;
padding: $large-spacing 15px; padding: $large-spacing 15px;
} }
flex-direction: column; flex-direction: column;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
&__hero { &__hero {
margin-bottom: 35px; margin-bottom: 35px;
}
&__hero-image {
max-width: 100%;
border-radius: 12px;
}
&__hero-source {
@include tiny-text;
line-height: 25px;
}
&__meta-title {
@include meta-title;
}
&__intro-wrapper {
position: relative;
}
&__intro {
> :deep(p) {
margin-bottom: $large-spacing;
@include lead-paragraph;
&:last-child {
margin-bottom: 0;
}
} }
> :deep(ul) { &__hero-image {
@include list-parent; max-width: 100%;
border-radius: 12px;
}
> li { &__hero-source {
@include list-child; @include tiny-text;
line-height: 25px;
}
&__header {
display: flex;
justify-content: flex-start;
align-items: stretch;
margin-bottom: 10px;
}
&__meta-title {
@include meta-title;
margin-right: 20px;
}
&__intro-wrapper {
position: relative;
}
&__intro {
> :deep(p) {
margin-bottom: $large-spacing;
@include lead-paragraph; @include lead-paragraph;
&:last-child {
margin-bottom: 0;
}
}
> :deep(ul) {
@include list-parent;
> li {
@include list-child;
@include lead-paragraph;
}
} }
} }
}
&__bookmark-actions { &__bookmark-actions {
margin-top: 3px; margin-top: 3px;
} }
&__objective-groups { &__objective-groups {
margin-bottom: 2 * $large-spacing; margin-bottom: 2 * $large-spacing;
}
} }
}
</style> </style>

View File

@ -101,6 +101,8 @@
function filterModules() { function filterModules() {
let filteredModules = props.modules; let filteredModules = props.modules;
if (selectedCategory.value === null) { if (selectedCategory.value === null) {
return props.modules; return props.modules;
} }
@ -108,14 +110,14 @@
// filter by Lehrjahr // filter by Lehrjahr
if (selectedCategory.value.name !== '---') { if (selectedCategory.value.name !== '---') {
filteredModules = filteredModules.filter((module) => { filteredModules = filteredModules.filter((module) => {
return module.category.id == selectedCategory.value.id; return module.category?.id == selectedCategory.value.id;
}); });
} }
//filter by Lernfeld //filter by Lernfeld
if (selectedLernfeld.value.name !== '---') { if (selectedLernfeld.value.name !== '---') {
filteredModules = filteredModules.filter((module) => { filteredModules = filteredModules.filter((module) => {
return module.categoryType.id == selectedLernfeld.value.id; return module.categoryType?.id == selectedLernfeld.value.id;
}); });
} }
updateLastModuleCategory(selectedCategory.value); updateLastModuleCategory(selectedCategory.value);

View File

@ -17,89 +17,88 @@
<p class="module-teaser__description"> <p class="module-teaser__description">
{{ teaser }} {{ teaser }}
</p> </p>
<span :value="attribute" v-for="attribute in [category.name, categoryType.name]"> <div class="module-teaser__pills">
<div class="module-teaser__module-category" v-if="attribute">{{attribute}}</div> <pill :text="category?.name"></pill>
</span> <pill :text="categoryType?.name"></pill>
</div>
</div> </div>
</router-link> </router-link>
</template> </template>
<script> <script>
export default { import Pill from "@/components/ui/Pill.vue";
props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage', 'category', 'categoryType'],
computed: { export default {
moduleLink() { components: {Pill},
if (this.slug) { props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage', 'category', 'categoryType'],
return {
name: 'module',
params: { computed: {
slug: this.slug, moduleLink() {
}, if (this.slug) {
}; return {
} else { name: 'module',
return {}; params: {
} slug: this.slug,
},
};
} else {
return {};
}
},
}, },
}, };
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import 'styles/helpers'; @import 'styles/helpers';
.module-teaser { .module-teaser {
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
border: 1px solid #e2e2e2; border: 1px solid #e2e2e2;
height: 390px; height: 390px;
max-width: 380px; max-width: 380px;
width: 100%;
border-radius: 12px;
overflow: hidden;
box-sizing: border-box;
cursor: pointer;
&--small {
height: 300px;
}
&__image {
width: 100%; width: 100%;
max-height: 150px; border-radius: 12px;
height: 150px; overflow: hidden;
background-position: center; box-sizing: border-box;
background-size: 100% auto; cursor: pointer;
background-repeat: no-repeat;
}
&__body { &--small {
padding: 20px; height: 300px;
}
&__meta-title {
color: $color-silver-dark;
margin-bottom: $large-spacing;
@include regular-text;
}
&__title {
@include heading-3;
margin-bottom: 5px;
}
&__description {
line-height: $default-line-height;
font-size: 1.2rem;
}
&__module-category {
background-color: rgba(129, 129, 129, 0.99); /* Replace with your desired background color */
color: #fff; /* Replace with your desired text color */
padding: 10px 20px;
border-radius: 40px; /* A high value to make it look like a pill */
display: inline-block; /* Ensures the pill takes only the necessary width */
margin-right: 10px;
margin-top: 20px;
} }
}
&__image {
width: 100%;
max-height: 150px;
height: 150px;
background-position: center;
background-size: 100% auto;
background-repeat: no-repeat;
}
&__body {
padding: 20px;
}
&__meta-title {
color: $color-silver-dark;
margin-bottom: $large-spacing;
@include regular-text;
}
&__title {
@include heading-3;
margin-bottom: 5px;
}
&__description {
line-height: $default-line-height;
font-size: 1.2rem;
}
&__pills {
margin-top: 20px;
}
}
</style> </style>

View File

@ -30,6 +30,8 @@ import Avatar from '@/components/profile/Avatar.vue';
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
const TrashIcon = defineAsyncComponent(() => import('@/components/icons/TrashIcon.vue')); const TrashIcon = defineAsyncComponent(() => import('@/components/icons/TrashIcon.vue'));
// TODO: Kann das mit me.ts zusammengeführt werden?
export default { export default {
components: { components: {
AvatarUploadForm, AvatarUploadForm,

View File

@ -0,0 +1,26 @@
<template>
<div class="pill" v-if="props.text">{{ props.text }}</div>
</template>
<script setup lang="ts">
const props = defineProps<{
text: [];
}>();
</script>
<style scoped lang="scss">
@import 'styles/helpers';
.pill {
background-color: white; /* Replace with your desired background color */
color: #333333; /* Replace with your desired text color */
padding: 10px 20px;
border-radius: 30px;
border: 1px solid silver;
display: inline-block; /* Ensures the pill takes only the necessary width */
margin-right: 10px;
margin-top: 2px;
@include small-text;
}
</style>

View File

@ -14,6 +14,7 @@ export interface Me {
team: any; team: any;
lastTopic: any; lastTopic: any;
readOnly: boolean; readOnly: boolean;
lastModuleCategory: any;
} }
export interface MeQuery { export interface MeQuery {
@ -30,6 +31,8 @@ export interface Location {
type RouteLocation = Location | string; type RouteLocation = Location | string;
// TODO: ME_QUERY existiert an einem weiteren Ort. Dieser sollte entfernt werden.
const defaultMe: MeQuery = { const defaultMe: MeQuery = {
me: { me: {
selectedClass: { selectedClass: {
@ -42,6 +45,7 @@ const defaultMe: MeQuery = {
team: null, team: null,
readOnly: false, readOnly: false,
lastTopic: undefined, lastTopic: undefined,
lastModuleCategory: undefined,
}, },
}; };
@ -76,6 +80,10 @@ const getCurrentClassName = (me: Me) => {
return currentClass ? currentClass.name : me.schoolClasses.length ? me.schoolClasses[0].name : ''; return currentClass ? currentClass.name : me.schoolClasses.length ? me.schoolClasses[0].name : '';
}; };
const getLastModuleCategory = (me: Me) => {
return me.lastModuleCategory;
}
const getMe = () => { const getMe = () => {
const { result } = useQuery(ME_QUERY); const { result } = useQuery(ME_QUERY);
@ -126,6 +134,10 @@ const meMixin = {
// @ts-ignore // @ts-ignore
return getCurrentClassName(this.$data.me); return getCurrentClassName(this.$data.me);
}, },
lastModuleCategory(): any {
// @ts-ignore
return getLastModuleCategory(this.$data.me);
}
}, },
apollo: { apollo: {

View File

@ -3,7 +3,7 @@ from django.utils import timezone
from graphene import relay from graphene import relay
from api.utils import get_object from api.utils import get_object
from books.models import Module, RecentModule from books.models import Module, RecentModule, ModuleCategory
from books.schema.nodes import ModuleNode from books.schema.nodes import ModuleNode
from users.models import SchoolClass, User from users.models import SchoolClass, User
from users.schema import PrivateUserNode from users.schema import PrivateUserNode
@ -115,6 +115,8 @@ class UpdateLastModuleCategory(relay.ClientIDMutation):
def mutate_and_get_payload(cls, root, info, **args): def mutate_and_get_payload(cls, root, info, **args):
user = info.context.user user = info.context.user
id = args.get('id') id = args.get('id')
module_category = get_object(ModuleCategory, id)
User.objects.filter(pk=user.id).update(last_module_category_id=id)
User.objects.filter(pk=user.id).update(last_module_category_id=module_category.id)
return cls(user=user) return cls(user=user)

View File

@ -108,7 +108,7 @@ class ModuleNode(DjangoObjectType):
@staticmethod @staticmethod
def resolve_objective_groups(parent, info, **kwargs): def resolve_objective_groups(parent, info, **kwargs):
return parent.objective_groups.all().prefetch_related("hidden_for") @ staticmethod return parent.objective_groups.all().prefetch_related("hidden_for")
def resolve_category(self, info, **kwargs): def resolve_category(self, info, **kwargs):
return ModuleCategory.objects.get(pk=self.category_id) if self.category_id else None return ModuleCategory.objects.get(pk=self.category_id) if self.category_id else None

View File

@ -104,6 +104,7 @@ class PrivateUserNode(DjangoObjectType):
"onboarding_visited", "onboarding_visited",
"team", "team",
"read_only", "read_only",
"last_module_category"
] ]
interfaces = (relay.Node,) interfaces = (relay.Node,)
@ -114,7 +115,6 @@ class PrivateUserNode(DjangoObjectType):
is_teacher = graphene.Boolean() is_teacher = graphene.Boolean()
old_classes = graphene.List(SchoolClassNode) old_classes = graphene.List(SchoolClassNode)
school_classes = graphene.List(SchoolClassNode) school_classes = graphene.List(SchoolClassNode)
last_module_category = graphene.Field(ModuleCategoryNode)
recent_modules = DjangoFilterConnectionField( recent_modules = DjangoFilterConnectionField(
ModuleNode, filterset_class=RecentModuleFilter ModuleNode, filterset_class=RecentModuleFilter
@ -163,8 +163,6 @@ class PrivateUserNode(DjangoObjectType):
def resolve_team(self, info, **kwargs): def resolve_team(self, info, **kwargs):
return self.team return self.team
def resolve_last_module_category(self, info, **kwargs):
return self.last_module_category
class ClassMemberNode(ObjectType): class ClassMemberNode(ObjectType):