diff --git a/com/api.py b/com/api.py new file mode 100644 index 00000000..64f375fd --- /dev/null +++ b/com/api.py @@ -0,0 +1,53 @@ +from datetime import timedelta + +import urllib3 +from django.core.cache import cache +from django.http import HttpResponse +from django.utils import timezone +from ics import Calendar, Event +from ninja_extra import ControllerBase, api_controller, route + +from com.models import NewsDate + + +@api_controller("/calendar") +class CalendarController(ControllerBase): + @route.get("/external.ics") + def calendar_external(self): + CACHE_KEY = "external_calendar" + if cached := cache.get(CACHE_KEY): + return HttpResponse( + cached, + content_type="text/calendar", + status=200, + ) + calendar = urllib3.request( + "GET", + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + ) + if calendar.status == 200: + cache.set(CACHE_KEY, calendar.data, 3600) # Cache for one hour + return HttpResponse( + calendar.data, + content_type="text/calendar", + status=calendar.status, + ) + + @route.get("/internal.ics") + def calendar_internal(self): + calendar = Calendar() + for news_date in NewsDate.objects.filter( + news__is_moderated=True, + start_date__lte=timezone.now() + timedelta(days=30), + ).prefetch_related("news"): + event = Event( + name=news_date.news.title, + begin=news_date.start_date, + end=news_date.end_date, + ) + calendar.events.add(event) + + return HttpResponse( + calendar.serialize().encode("utf-8"), + content_type="text/calendar", + ) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts new file mode 100644 index 00000000..f88b9b0f --- /dev/null +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -0,0 +1,75 @@ +import { makeUrl } from "#core:utils/api"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; +import { Calendar } from "@fullcalendar/core"; +import enLocale from "@fullcalendar/core/locales/en-gb"; +import frLocale from "@fullcalendar/core/locales/fr"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import iCalendarPlugin from "@fullcalendar/icalendar"; +import listPlugin from "@fullcalendar/list"; +import { calendarCalendarExternal, calendarCalendarInternal } from "#openapi"; + +@registerComponent("ics-calendar") +export class IcsCalendar extends inheritHtmlElement("div") { + static observedAttributes = ["locale"]; + private calendar: Calendar; + private locale = "en"; + + attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) { + if (name !== "locale") { + return; + } + + this.locale = newValue; + } + + isMobile() { + return window.innerWidth < 765; + } + + currentView() { + // Get view type based on viewport + return this.isMobile() ? "listMonth" : "dayGridMonth"; + } + + currentToolbar() { + if (this.isMobile()) { + return { + left: "prev,next", + center: "title", + right: "", + }; + } + return { + left: "prev,next today", + center: "title", + right: "dayGridMonth,dayGridWeek,dayGridDay", + }; + } + + async connectedCallback() { + super.connectedCallback(); + this.calendar = new Calendar(this.node, { + plugins: [dayGridPlugin, iCalendarPlugin, listPlugin], + locales: [frLocale, enLocale], + height: "auto", + locale: this.locale, + initialView: this.currentView(), + headerToolbar: this.currentToolbar(), + eventSources: [ + { + url: await makeUrl(calendarCalendarInternal), + format: "ics", + }, + { + url: await makeUrl(calendarCalendarExternal), + format: "ics", + }, + ], + windowResize: () => { + this.calendar.changeView(this.currentView()); + this.calendar.setOption("headerToolbar", this.currentToolbar()); + }, + }); + this.calendar.render(); + } +} diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss new file mode 100644 index 00000000..bb858dd5 --- /dev/null +++ b/com/static/com/components/ics-calendar.scss @@ -0,0 +1,60 @@ +@import "core/static/core/colors"; + + +:root { + --fc-button-border-color: #fff; + --fc-button-hover-border-color: #fff; + --fc-button-active-border-color: #fff; + --fc-button-text-color: #fff; + --fc-button-bg-color: #1a78b3; + --fc-button-active-bg-color: #15608F; + --fc-button-hover-bg-color: #15608F; + --fc-today-bg-color: rgba(26, 120, 179, 0.1); + --fc-border-color: #DDDDDD; + --sc-main-background-color: #f9fafb; + --sc-main-padding: 5px; + --sc-main-border: 0px solid #DDDDDD; + --sc-main-border-radius: 0px; + --sc-body-font-family: Roboto; + --sc-title-font-family: Roboto; + --sc-body-font-size: 16px; + --sc-title-font-size: 28px; + --sc-body-font-weight: 400; + --sc-title-font-weight: 500; + --sc-title-font-color: #111111; + --sc-base-body-font-color: #222222; + --sc-title-font-style: normal; + --sc-body-font-style: normal; + --sc-event-dot-color: #1a78b3; + --sc-button-border: 1px solid #ffffff; + --sc-button-border-radius: 4px; + --sc-button-icons-size: 22px; + --sc-grid-event-white-space: nowrap; + --sc-block-event-background-color-hovered: rgb(245, 245, 245); + --sc-block-event-border: 1px solid rgba(255, 255, 255, 0); + --sc-block-event-border-radius: 2.5px; + --sc-dot-event-background-color: rgba(255, 255, 255, 0); + --sc-dot-event-background-color-hovered: rgb(245, 245, 245); + --sc-dot-event-text-color: #222222; + --sc-dot-event-border: 1px solid rgba(255, 255, 255, 0); + --sc-dot-event-border-radius: 2.5px; + --sc-grid-day-header-background-color: rgba(255, 255, 255, 0); + --sc-list-day-header-background-color: rgba(208, 208, 208, 0.3); + --sc-inner-calendar-background-color: rgba(255, 255, 255, 0); + --sc-past-day-background-color: rgba(255, 255, 255, 0); + --sc-future-day-background-color: rgba(255, 255, 255, 0); + --sc-disabled-day-background-color: rgba(208, 208, 208, 0.3); + --sc-event-overlay-background-color: #FFFFFF; + --sc-event-overlay-padding: 20px; + --sc-event-overlay-border: 1px solid #EEEEEE; + --sc-event-overlay-border-radius: 4px; + --sc-event-overlay-primary-icon-color: #1a78b3; + --sc-event-overlay-secondary-icon-color: #000000; + --sc-event-overlay-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); + --sc-event-overlay-max-width: 600px; +} + +ics-calendar { + border: none; + box-shadow: none; +} \ No newline at end of file diff --git a/com/static/com/css/news-detail.scss b/com/static/com/css/news-detail.scss new file mode 100644 index 00000000..0a07e62d --- /dev/null +++ b/com/static/com/css/news-detail.scss @@ -0,0 +1,66 @@ +@import "core/static/core/colors"; + +#news_details { + display: inline-block; + margin-top: 20px; + padding: 0.4em; + width: 80%; + background: $white-color; + + h4 { + margin-top: 1em; + text-transform: uppercase; + } + + .club_logo { + display: inline-block; + text-align: center; + width: 19%; + float: left; + min-width: 15em; + margin: 0; + + img { + max-height: 15em; + max-width: 12em; + display: block; + margin: 0 auto; + margin-bottom: 10px; + } + } + + .share_button { + border: none; + color: white; + padding: 0.5em 1em; + text-align: center; + text-decoration: none; + font-size: 1.2em; + border-radius: 2px; + float: right; + display: block; + margin-left: 0.3em; + + &:hover { + color: lightgrey; + } + } + + .facebook { + background: $faceblue; + } + + .twitter { + background: $twitblue; + } + + .news_meta { + margin-top: 10em; + font-size: small; + } +} + +.helptext { + margin-top: 10px; + display: block; +} \ No newline at end of file diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss new file mode 100644 index 00000000..a33e6315 --- /dev/null +++ b/com/static/com/css/news-list.scss @@ -0,0 +1,298 @@ +@import "core/static/core/colors"; +@import "core/static/core/devices"; + +#news { + display: flex; + + @media (max-width: 800px) { + flex-direction: column; + } + + .news_column { + display: inline-block; + margin: 0; + vertical-align: top; + } + + #news_admin { + margin-bottom: 1em; + } + + #right_column { + flex: 20%; + float: right; + margin: 0.2em; + } + + #left_column { + flex: 79%; + margin: 0.2em; + + h3 { + background: $second-color; + box-shadow: $shadow-color 1px 1px 1px; + padding: 0.4em; + margin: 0 0 0.5em 0; + text-transform: uppercase; + font-size: 1.1em; + + &:not(:first-of-type) { + margin: 2em 0 1em 0; + } + } + } + + @media screen and (max-width: $small-devices) { + + #left_column, + #right_column { + flex: 100%; + } + } + + /* AGENDA/BIRTHDAYS */ + #agenda, + #birthdays { + display: block; + width: 100%; + background: white; + font-size: 70%; + margin-bottom: 1em; + + #agenda_title, + #birthdays_title { + margin: 0; + border-radius: 5px 5px 0 0; + box-shadow: $shadow-color 1px 1px 1px; + padding: 0.5em; + font-weight: bold; + font-size: 150%; + text-align: center; + text-transform: uppercase; + background: $second-color; + } + + #agenda_content { + overflow: auto; + box-shadow: $shadow-color 1px 1px 1px; + height: 20em; + } + + #agenda_content, + #birthdays_content { + .agenda_item { + padding: 0.5em; + margin-bottom: 0.5em; + + &:nth-of-type(even) { + background: $secondary-neutral-light-color; + } + + .agenda_time { + font-size: 90%; + color: grey; + } + + .agenda_item_content { + p { + margin-top: 0.2em; + } + } + } + + ul.birthdays_year { + margin: 0; + list-style-type: none; + font-weight: bold; + + >li { + padding: 0.5em; + + &:nth-child(even) { + background: $secondary-neutral-light-color; + } + } + + ul { + margin: 0; + margin-left: 1em; + list-style-type: square; + list-style-position: inside; + font-weight: normal; + } + } + } + } + + /* END AGENDA/BIRTHDAYS */ + + /* EVENTS TODAY AND NEXT FEW DAYS */ + .news_events_group { + box-shadow: $shadow-color 1px 1px 1px; + margin-left: 1em; + margin-bottom: 0.5em; + + .news_events_group_date { + display: table-cell; + padding: 0.6em; + vertical-align: middle; + background: $primary-neutral-dark-color; + color: $white-color; + text-transform: uppercase; + text-align: center; + font-weight: bold; + font-family: monospace; + font-size: 1.4em; + border-radius: 7px 0 0 7px; + + div { + margin: 0 auto; + + .day { + font-size: 1.5em; + } + } + } + + .news_events_group_items { + display: table-cell; + width: 100%; + + .news_event:nth-of-type(odd) { + background: white; + } + + .news_event:nth-of-type(even) { + background: $primary-neutral-light-color; + } + + .news_event { + display: block; + padding: 0.4em; + + &:not(:last-child) { + border-bottom: 1px solid grey; + } + + div { + margin: 0.2em; + } + + h4 { + margin-top: 1em; + text-transform: uppercase; + } + + .club_logo { + float: left; + min-width: 7em; + max-width: 9em; + margin: 0; + margin-right: 1em; + margin-top: 0.8em; + + img { + max-height: 6em; + max-width: 8em; + display: block; + margin: 0 auto; + } + } + + .news_date { + font-size: 100%; + } + + .news_content { + clear: left; + + .button_bar { + text-align: right; + + .fb { + color: $faceblue; + } + + .twitter { + color: $twitblue; + } + } + } + } + } + } + + /* END EVENTS TODAY AND NEXT FEW DAYS */ + + /* COMING SOON */ + .news_coming_soon { + display: list-item; + list-style-type: square; + list-style-position: inside; + margin-left: 1em; + padding-left: 0; + + a { + font-weight: bold; + text-transform: uppercase; + } + + .news_date { + font-size: 0.9em; + } + } + + /* END COMING SOON */ + + /* NOTICES */ + .news_notice { + margin: 0 0 1em 1em; + padding: 0.4em; + padding-left: 1em; + background: $secondary-neutral-light-color; + box-shadow: $shadow-color 0 0 2px; + border-radius: 18px 5px 18px 5px; + + h4 { + margin: 0; + } + + .news_content { + margin-left: 1em; + } + } + + /* END NOTICES */ + + /* CALLS */ + .news_call { + margin: 0 0 1em 1em; + padding: 0.4em; + padding-left: 1em; + background: $secondary-neutral-light-color; + border: 1px solid grey; + box-shadow: $shadow-color 1px 1px 1px; + + h4 { + margin: 0; + } + + .news_date { + font-size: 0.9em; + } + + .news_content { + margin-left: 1em; + } + } + + /* END CALLS */ + + .news_empty { + margin-left: 1em; + } + + .news_date { + color: grey; + } +} \ No newline at end of file diff --git a/com/static/com/css/posters.scss b/com/static/com/css/posters.scss new file mode 100644 index 00000000..26cf2b91 --- /dev/null +++ b/com/static/com/css/posters.scss @@ -0,0 +1,230 @@ +#poster_list, +#screen_list, +#poster_edit, +#screen_edit { + position: relative; + + #title { + position: relative; + padding: 10px; + margin: 10px; + border-bottom: 2px solid black; + + h3 { + display: flex; + justify-content: center; + align-items: center; + } + + #links { + position: absolute; + display: flex; + bottom: 5px; + + &.left { + left: 0; + } + + &.right { + right: 0; + } + + .link { + padding: 5px; + padding-left: 20px; + padding-right: 20px; + margin-left: 5px; + border-radius: 20px; + background-color: hsl(40, 100%, 50%); + color: black; + + &:hover { + color: black; + background-color: hsl(40, 58%, 50%); + } + + &.delete { + background-color: hsl(0, 100%, 40%); + } + } + } + } + + #posters, + #screens { + position: relative; + display: flex; + flex-wrap: wrap; + + #no-posters, + #no-screens { + display: flex; + justify-content: center; + align-items: center; + } + + .poster, + .screen { + min-width: 10%; + max-width: 20%; + display: flex; + flex-direction: column; + margin: 10px; + border: 2px solid darkgrey; + border-radius: 4px; + padding: 10px; + background-color: lightgrey; + + * { + display: flex; + justify-content: center; + align-items: center; + } + + .name { + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + } + + .image { + flex-grow: 1; + position: relative; + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + + img { + max-height: 20vw; + max-width: 100%; + } + + &:hover { + &::before { + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + top: 0; + left: 0; + z-index: 10; + content: "Click to expand"; + color: white; + background-color: rgba(black, 0.5); + } + } + } + + .dates { + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + + * { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + margin-left: 5px; + margin-right: 5px; + } + + .begin, + .end { + width: 48%; + } + + .begin { + border-right: 1px solid whitesmoke; + padding-right: 2%; + } + } + + .edit, + .moderate, + .slideshow { + padding: 5px; + border-radius: 20px; + background-color: hsl(40, 100%, 50%); + color: black; + + &:hover { + color: black; + background-color: hsl(40, 58%, 50%); + } + + &:nth-child(2n) { + margin-top: 5px; + margin-bottom: 5px; + } + } + + .tooltip { + visibility: hidden; + width: 120px; + background-color: hsl(210, 20%, 98%); + color: hsl(0, 0%, 0%); + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: 10; + + ul { + margin-left: 0; + display: inline-block; + + li { + display: list-item; + list-style-type: none; + } + } + } + + &.not_moderated { + border: 1px solid red; + } + + &:hover .tooltip { + visibility: visible; + } + } + } + + #view { + position: fixed; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + z-index: 10; + visibility: hidden; + background-color: rgba(10, 10, 10, 0.9); + overflow: hidden; + + &.active { + visibility: visible; + } + + #placeholder { + width: 80vw; + height: 80vh; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + + img { + max-width: 100%; + max-height: 100%; + } + } + } +} \ No newline at end of file diff --git a/com/templates/com/news_detail.jinja b/com/templates/com/news_detail.jinja index cbfa596c..238515ed 100644 --- a/com/templates/com/news_detail.jinja +++ b/com/templates/com/news_detail.jinja @@ -11,6 +11,11 @@ {{ gen_news_metatags(news) }} {% endblock %} + +{% block additional_css %} + +{% endblock %} + {% block content %}
{% trans %}Back to news{% endtrans %}