Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/samfundet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
RecruitmentInterviewAvailability,
RecruitmentPositionSharedInterviewGroup,
)
from .models.site_banner import SiteBanner

# Common fields:
# ordering = []
Expand All @@ -90,6 +91,15 @@ class OccupiedTimeAdmin(admin.ModelAdmin):
list_select_related = True


@admin.register(SiteBanner)
class SiteBannerAdmin(CustomBaseAdmin):
list_display = ['id', 'is_active', 'start_at', 'end_at', 'updated_at']
list_filter = ['is_active']
search_fields = ['text_nb', 'text_en']
ordering = ['-start_at', '-created_at']
list_select_related = True


@admin.register(User)
class UserAdmin(CustomGuardedUserAdmin):
sortable_by = [
Expand Down
38 changes: 38 additions & 0 deletions backend/samfundet/migrations/0006_sitebanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.2.11 on 2026-03-17 20:40

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0005_medlemsinfo'),
]

operations = [
migrations.CreateModel(
name='SiteBanner',
fields=[
('version', models.PositiveIntegerField(blank=True, default=0, editable=False, null=True)),
('created_at', models.DateTimeField(blank=True, editable=False, null=True)),
('updated_at', models.DateTimeField(blank=True, editable=False, null=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('text_nb', models.CharField(max_length=80)),
('text_en', models.CharField(max_length=80)),
('url', models.CharField(blank=True, max_length=500, null=True)),
('new_tab', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('start_at', models.DateTimeField(blank=True, null=True)),
('end_at', models.DateTimeField(blank=True, null=True)),
Comment thread
StenOskar marked this conversation as resolved.
Outdated
('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('updated_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Site banner',
'verbose_name_plural': 'Site banners',
},
),
]
2 changes: 2 additions & 0 deletions backend/samfundet/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Organization,
UserPreference,
)
from .site_banner import SiteBanner

__all__ = [
'User',
Expand All @@ -26,4 +27,5 @@
'Image',
'Profile',
'UserPreference',
'SiteBanner',
]
45 changes: 45 additions & 0 deletions backend/samfundet/models/site_banner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

import uuid

from django.db import models
from django.utils import timezone

from root.utils.mixins import CustomBaseModel


class SiteBanner(CustomBaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
Comment thread
StenOskar marked this conversation as resolved.
Outdated

text_nb = models.CharField(max_length=80)
text_en = models.CharField(max_length=80)
Comment thread
StenOskar marked this conversation as resolved.
Outdated
Comment thread
StenOskar marked this conversation as resolved.
Outdated

url = models.CharField(max_length=500, blank=True, null=True)
new_tab = models.BooleanField(default=False)

is_active = models.BooleanField(default=True)
start_at = models.DateTimeField(null=True, blank=True)
end_at = models.DateTimeField(null=True, blank=True)

class Meta:
verbose_name = 'Site banner'
verbose_name_plural = 'Site banners'

def __str__(self) -> str:
return f'{self.text_nb[:40]}'

def is_currently_active(self, *, now: timezone.datetime | None = None) -> bool:
Comment thread
StenOskar marked this conversation as resolved.
Outdated
now = now or timezone.now()
if not self.is_active:
return False
if self.start_at and self.start_at > now:
return False
return not (self.end_at and self.end_at < now)
Comment thread
StenOskar marked this conversation as resolved.
Outdated

@classmethod
def active(cls) -> models.QuerySet:
now = timezone.now()
return cls.objects.filter(is_active=True).filter(
models.Q(start_at__isnull=True) | models.Q(start_at__lte=now),
models.Q(end_at__isnull=True) | models.Q(end_at__gte=now),
)
17 changes: 17 additions & 0 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
RecruitmentInterviewAvailability,
RecruitmentPositionSharedInterviewGroup,
)
from .models.site_banner import SiteBanner
from .models.model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices

if TYPE_CHECKING:
Expand Down Expand Up @@ -284,6 +285,22 @@ class Meta:
fields = '__all__'


class SiteBannerSerializer(CustomBaseSerializer):
class Meta:
model = SiteBanner
fields = [
'id',
'version',
'text_nb',
'text_en',
'url',
'new_tab',
'is_active',
'start_at',
'end_at',
]


class ChangePasswordSerializer(serializers.Serializer):
current_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
Expand Down
2 changes: 2 additions & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import samfundet.view.mdb_views
import samfundet.view.user_views
import samfundet.view.event_views
import samfundet.view.site_banners
import samfundet.view.sulten_views
import samfundet.view.general_views
from samfundet.view import billig_views
Expand Down Expand Up @@ -45,6 +46,7 @@
router.register('organizations', samfundet.view.general_views.OrganizationView, 'organizations')
router.register('merch', samfundet.view.general_views.MerchView, 'merch')
router.register('role', samfundet.view.general_views.RoleView, 'role')
router.register('site-banners', samfundet.view.site_banners.SiteBannerView, 'site_banners')

########## Recruitment ##########
router.register('recruitment', recruitment_views.RecruitmentView, 'recruitment')
Expand Down
25 changes: 25 additions & 0 deletions backend/samfundet/view/site_banners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from rest_framework import decorators
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import AllowAny

from django.db import models

from samfundet.serializers import SiteBannerSerializer
from samfundet.models.site_banner import SiteBanner


class SiteBannerView(ReadOnlyModelViewSet):
permission_classes = [AllowAny]
serializer_class = SiteBannerSerializer

def get_queryset(self) -> models.QuerySet:
return SiteBanner.active().order_by('-start_at', '-created_at')

@decorators.action(detail=False, methods=['get'], url_path='active')
def active(self, request: Request) -> Response:
serializer = self.get_serializer(self.get_queryset(), many=True)
return Response(serializer.data)
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ $navbar-link-transparent-hover-gradient-bottom: transparent;
/* Main navbar */
#navbar_container {
position: fixed;
top: 0;
top: var(--site-banner-offset, 0);
transition: top 150ms ease;
width: 100%;
height: $navbar-height;
background: $navbar-bg-dark;
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/Components/SamfOutlet/SamfOutlet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react';
import { Outlet } from 'react-router';
import { Outlet, useLocation } from 'react-router';
import { Navbar } from '~/Components/NavbarSamfThree/Navbar';
import { SiteBanner } from '~/Components/SiteBanner/SiteBanner';
import { Footer } from '../Footer';
import styles from './SamfOutlet.module.scss';

Expand All @@ -13,11 +14,16 @@ export function SamfOutlet() {
}

export function SamfLayout({ children }: { children: ReactNode }) {
const location = useLocation();

const showBanner = location.pathname === '/' || location.pathname === '/home/';

return (
<>
{/* TODO: Uncomment the following line when samf4 navbar is enabled */}
{/* <Navbar /> */}
{/* TODO: Remove the following line when samf4 navbar is enabled */}
{showBanner && <SiteBanner />}
<Navbar />
<div className={styles.navbar_outlet}>{children}</div>
<Footer />
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/Components/SiteBanner/SiteBanner.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@use 'src/constants' as *;

.banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1100;
background: $red-samf;
color: $white;
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem 1rem;
overflow: hidden;
transition:
max-height 180ms ease,
opacity 180ms ease,
padding-top 80ms ease
}

.hidden {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
pointer-events: none;
}

.content {
width: 100%;
max-width: 1200px;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
text-align: center;
overflow: hidden;
}

.text {
flex: 1;
min-width: 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.links {
Comment thread
StenOskar marked this conversation as resolved.
Outdated
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}

.link {
color: inherit;
text-decoration: underline;

&:focus-visible {
outline: 2px solid currentcolor;
outline-offset: 2px;
}
}

@media (max-width: 900px) {
.content {
flex-direction: column;
gap: 0.35rem;
}

.text {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}

.links {
flex-wrap: wrap;
justify-content: center;
}
}
Loading
Loading