Frontend basic tour
Git repo that reference the code tour : https://framagit.org/framasoft/mobilizon .
On branch or commit "09cc5d1bd1ecd24e1e879b0392cf5830b2581c74" .
Html entry point : public/index.html
This public/index.html
is the first file that is loaded by the browser.
It loads the bundled VueJs application as a script.
The rendering of the application is then delegated to javascript.
The VueJs app is "injected" inside the <div is="app"></div>
element.
The bundled javascript script will be inserted in the index.html
file in place of the comment : <!-- built files will be auto injected -->
Browse code : js/public/index.html
Reveal code
<! DOCTYPE html > < html lang = " en" > < 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" /> < link rel = " icon" href = " <%= BASE_URL %>favicon.ico" /> < meta name = " server-injected-data" /> </ head> < body> < noscript> < strong > We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</ strong > </ noscript> < div id = " app" > </ div> </ body> </ html>
The main.ts
VueJs entry point
This main.ts
is the root Typescript file of the VueJs application.
It initializes the root component. It is automatically injected at the end of the "public/index.html"
file after being bundled.
It will load the various required dependencies such as :
The App.vue
entry point template
This entry point is fed with a few features such as
A router
to load the appropriate Vue components depending on the browser's URL
apolloProvider
to allow to communicate with the server GraphQL API
i18n
to handle text internationalization
Browse code : js/src/main.ts
Reveal code
import Vue from "vue" ; import Buefy from "buefy" ; import Component from "vue-class-component" ; import VueScrollTo from "vue-scrollto" ; import VueMeta from "vue-meta" ; import VTooltip from "v-tooltip" ; import App from "./App.vue" ; import router from "./router" ; import { NotifierPlugin } from "./plugins/notifier" ; import filters from "./filters" ; import { i18n } from "./utils/i18n" ; import apolloProvider from "./vue-apollo" ; import "./registerServiceWorker" ; Vue. config. productionTip = false ; Vue. use ( Buefy) ; Vue. use ( NotifierPlugin) ; Vue. use ( filters) ; Vue. use ( VueMeta) ; Vue. use ( VueScrollTo) ; Vue. use ( VTooltip) ; Component. registerHooks ( [ "beforeRouteEnter" , "beforeRouteLeave" , "beforeRouteUpdate" , ] ) ; new Vue ( { router, apolloProvider, el: "#app" , template: "<App/>" , components: { App } , render : ( h) => h ( App) , i18n, } ) ;
App.vue entry point and <main>
The App.vue
is the entry point single file component (root component) of the Single Page Application (SPA).
The <main>
component that is resolved by the router. The component <router-view/>
is replaced depending on the view component resolved by the router.
The component to replace <router-view/>
depends on the rules found in the router/index.ts
file.
Go further
Browse code : js/src/App.vue
Reveal code
< template> < div id = " mobilizon" > < NavBar /> < div v-if = " config && config.demoMode" > < b-message class = " container" type = " is-danger" :title = " $t(' Warning' ).toLocaleUpperCase()" closable aria-close-label = " Close" > < p> {{ $t("This is a demonstration site to test Mobilizon.") }} < b> {{ $t("Please do not use it in any real way.") }}</ b> {{ $t( "This website isn't moderated and the data that you enter will be automatically destroyed every day at 00:01 (Paris timezone)." ) }} </ p> </ b-message> </ div> < error v-if = " error" :error = " error" /> < main v-else > < transition name = " fade" mode = " out-in" > < router-view /> </ transition> </ main> < mobilizon-footer /> </ div> </ template> < script lang = " ts" > import { Component, Vue } from "vue-property-decorator" ; import NavBar from "./components/NavBar.vue" ; import { AUTH_ACCESS_TOKEN , AUTH_USER_EMAIL , AUTH_USER_ID , AUTH_USER_ROLE , } from "./constants" ; import { CURRENT_USER_CLIENT , UPDATE_CURRENT_USER_CLIENT , } from "./graphql/user" ; import Footer from "./components/Footer.vue" ; import Logo from "./components/Logo.vue" ; import { initializeCurrentActor } from "./utils/auth" ; import { CONFIG } from "./graphql/config" ; import { IConfig } from "./types/config.model" ; import { ICurrentUser } from "./types/current-user.model" ; import jwt_decode, { JwtPayload } from "jwt-decode" ; import { refreshAccessToken } from "./apollo/utils" ; @Component ( { apollo: { currentUser: CURRENT_USER_CLIENT , config: CONFIG , } , components: { Logo, NavBar, error : ( ) => import ( "./components/Error.vue" ) , "mobilizon-footer" : Footer, } , metaInfo ( ) { return { titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class App extends Vue { config! : IConfig; currentUser! : ICurrentUser; error: Error | null = null ; online = true ; interval: number | undefined = undefined ; async created ( ) : Promise< void > { if ( await this . initializeCurrentUser ( ) ) { await initializeCurrentActor ( this . $apollo. provider. defaultClient) ; } } errorCaptured ( error: Error) : void { this . error = error; } private async initializeCurrentUser ( ) { const userId = localStorage. getItem ( AUTH_USER_ID ) ; const userEmail = localStorage. getItem ( AUTH_USER_EMAIL ) ; const accessToken = localStorage. getItem ( AUTH_ACCESS_TOKEN ) ; const role = localStorage. getItem ( AUTH_USER_ROLE ) ; if ( userId && userEmail && accessToken && role) { return this . $apollo. mutate ( { mutation: UPDATE_CURRENT_USER_CLIENT , variables: { id: userId, email: userEmail, isLoggedIn: true , role, } , } ) ; } return false ; } mounted ( ) : void { this . online = window. navigator. onLine; window. addEventListener ( "offline" , ( ) => { this . online = false ; this . showOfflineNetworkWarning ( ) ; console. debug ( "offline" ) ; } ) ; window. addEventListener ( "online" , ( ) => { this . online = true ; console. debug ( "online" ) ; } ) ; document. addEventListener ( "refreshApp" , ( event: Event ) => { this . $buefy. snackbar. open ( { queue: false , indefinite: true , type: "is-secondary" , actionText: this . $t ( "Update app" ) as string, cancelText: this . $t ( "Ignore" ) as string, message: this . $t ( "A new version is available." ) as string, onAction : async ( ) => { const detail = event. detail; const registration = detail as ServiceWorkerRegistration; try { await this . refreshApp ( registration) ; window. location. reload ( ) ; } catch ( err) { console. error ( err) ; this . $notifier. error ( this . $t ( "An error has occured while refreshing the page." ) as string ) ; } } , } ) ; } ) ; this . interval = setInterval ( async ( ) => { const accessToken = localStorage. getItem ( AUTH_ACCESS_TOKEN ) ; if ( accessToken) { const token = jwt_decode< JwtPayload> ( accessToken) ; if ( token?. exp !== undefined && new Date ( token. exp * 1000 - 60000 ) < new Date ( ) ) { refreshAccessToken ( this . $apollo. getClient ( ) ) ; } } } , 60000 ) ; } private async refreshApp ( registration: ServiceWorkerRegistration ) : Promise< any> { const worker = registration. waiting; if ( ! worker) { return Promise. resolve ( ) ; } console. debug ( "Doing worker.skipWaiting()." ) ; return new Promise ( ( resolve, reject ) => { const channel = new MessageChannel ( ) ; channel. port1. onmessage = ( event ) => { console. debug ( "Done worker.skipWaiting()." ) ; if ( event. data. error) { reject ( event. data) ; } else { resolve ( event. data) ; } } ; console. debug ( "calling skip waiting" ) ; worker?. postMessage ( { type: "skip-waiting" } , [ channel. port2] ) ; } ) ; } showOfflineNetworkWarning ( ) : void { this . $notifier. error ( this . $t ( "You are offline" ) as string) ; } unmounted ( ) : void { clearInterval ( this . interval) ; this . interval = undefined ; } } </ script> < style lang = " scss" > @import "variables" ; $mdi-font-path : "~@mdi/font/fonts" ; @import "~@mdi/font/scss/materialdesignicons" ; @import "common" ; #mobilizon { min-height : 100vh; display : flex; flex-direction : column; main { flex-grow : 1; } } </ style>
Basic URL routing
If the URL typed in the browser is /
(ex. https://mobilizon.fr/
),
the router resolves to the Home
component.
It means that the <main></main>
element of the App.vue
entry point will be replaced with the views/Home.vue
component.
Since it does not require authentication, even if no user is authenticated, the Home
view will be rendered.
Go further
Browse code : js/src/router/index.ts
Reveal code
import Vue from "vue" ; import Router, { Route } from "vue-router" ; import VueScrollTo from "vue-scrollto" ; import { PositionResult } from "vue-router/types/router.d" ; import { ImportedComponent } from "vue/types/options" ; import Home from "../views/Home.vue" ; import { eventRoutes } from "./event" ; import { actorRoutes } from "./actor" ; import { errorRoutes } from "./error" ; import { authGuardIfNeeded } from "./guards/auth-guard" ; import { settingsRoutes } from "./settings" ; import { groupsRoutes } from "./groups" ; import { discussionRoutes } from "./discussion" ; import { userRoutes } from "./user" ; import RouteName from "./name" ; Vue. use ( Router) ; function scrollBehavior ( to: Route, from: Route, savedPosition: any ) : PositionResult | undefined | null { if ( to. hash) { VueScrollTo. scrollTo ( to. hash, 700 ) ; return { selector: to. hash, offset: { x: 0 , y: 10 } , } ; } if ( savedPosition) { return savedPosition; } return { x: 0 , y: 0 } ; } export const routes = [ ... userRoutes, ... eventRoutes, ... settingsRoutes, ... actorRoutes, ... groupsRoutes, ... discussionRoutes, ... errorRoutes, { path: "/search" , name: RouteName. SEARCH , component: ( ) : Promise < ImportedComponent> => import ( "@/views/Search.vue" ) , props: true , meta: { requiredAuth: false } , } , { path: "/" , name: RouteName. HOME , component: Home, meta: { requiredAuth: false } , } , { path: "/about" , name: RouteName. ABOUT , component: ( ) : Promise < ImportedComponent> => import ( "@/views/About.vue" ) , meta: { requiredAuth: false } , redirect: { name: RouteName. ABOUT_INSTANCE } , children: [ { path: "instance" , name: RouteName. ABOUT_INSTANCE , component: ( ) : Promise < ImportedComponent> => import ( "@/views/About/AboutInstance.vue" ) , } , { path: "/terms" , name: RouteName. TERMS , component: ( ) : Promise < ImportedComponent> => import ( "@/views/About/Terms.vue" ) , meta: { requiredAuth: false } , } , { path: "/privacy" , name: RouteName. PRIVACY , component: ( ) : Promise < ImportedComponent> => import ( "@/views/About/Privacy.vue" ) , meta: { requiredAuth: false } , } , { path: "/rules" , name: RouteName. RULES , component: ( ) : Promise < ImportedComponent> => import ( "@/views/About/Rules.vue" ) , meta: { requiredAuth: false } , } , { path: "/glossary" , name: RouteName. GLOSSARY , component: ( ) : Promise < ImportedComponent> => import ( "@/views/About/Glossary.vue" ) , meta: { requiredAuth: false } , } , ] , } , { path: "/interact" , name: RouteName. INTERACT , component: ( ) : Promise < ImportedComponent> => import ( "@/views/Interact.vue" ) , meta: { requiredAuth: false } , } , { path: "/auth/:provider/callback" , name: "auth-callback" , component: ( ) : Promise < ImportedComponent> => import ( "@/views/User/ProviderValidation.vue" ) , } , { path: "/welcome/:step?" , name: RouteName. WELCOME_SCREEN , component: ( ) : Promise < ImportedComponent> => import ( "@/views/User/SettingsOnboard.vue" ) , meta: { requiredAuth: true } , props: ( route: Route) : Record< string , unknown > => { const step = Number. parseInt ( route. params. step, 10 ) ; if ( Number. isNaN ( step) ) { return { step: 1 } ; } return { step } ; } , } , { path: "/404" , name: RouteName. PAGE_NOT_FOUND , component: ( ) : Promise < ImportedComponent> => import ( "../views/PageNotFound.vue" ) , meta: { requiredAuth: false } , } , { path: "*" , redirect: { name: RouteName. PAGE_NOT_FOUND } , } , ] ; const router = new Router ( { scrollBehavior, mode: "history" , base: "/" , routes, } ) ; router. beforeEach ( authGuardIfNeeded) ; router. afterEach ( ( ) => { try { if ( router. app. $children[ 0 ] ) { router. app. $children[ 0 ] . error = null ; } } catch ( e) { console . error ( e) ; } } ) ; export default router;
Conditional rendering
The v-if
directive tells if an element is rendered or not.
For instance, if the user is not registered of logged in, this section is shown.
To know if the user is registered of logged in, config
and currentUser
variables are looked up.
The presence of currentUser
variable is the result of a GraphQL query (see CURRENT_USER_CLIENT
).
The presence of config
variable is the result of a GraphQL query (see CONFIG
).
Go further
Browse code : js/src/views/Home.vue
Reveal code
< template> < div id = " homepage" > < section class = " hero" :class = " { webp: supportsWebPFormat }" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " hero-body" > < div class = " container" > < h1 class = " title" > {{ config.slogan || $t("Gather â‹… Organize â‹… Mobilize") }} </ h1> < p v-html = " $t(' Join <b>{instance}</b>, a Mobilizon instance' , { instance: config.name, }) " /> < p class = " instance-description" > {{ config.description }}</ p> < b-message type = " is-danger" v-if = " !config.registrationsOpen" > {{ $t("Unfortunately, this instance isn't opened to registrations") }}</ b-message> < div class = " buttons" > < b-button type = " is-primary" tag = " router-link" :to = " { name: RouteName.REGISTER }" v-if = " config.registrationsOpen" > {{ $t("Create an account") }}</ b-button > < b-button type = " is-text" tag = " router-link" :to = " { name: RouteName.ABOUT }" > {{ $t("Learn more about {instance}", { instance: config.name }) }} </ b-button> </ div> </ div> </ div> </ section> < div id = " recent_events" class = " container section" v-if = " config && (!currentUser.id || !currentActor.id)" > < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < b-loading :active.sync = " $apollo.loading" /> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < EventCard :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}</ b-message> </ section> </ div> < div id = " picture" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " picture-container" > < picture> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.webp" type = " image/webp" /> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.webp" type = " image/webp" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.webp" type = " image/webp" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.jpg" type = " image/jpeg" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.webp" type = " image/webp" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.jpg" type = " image/jpeg" /> < img src = " /img/pics/homepage-1024w.jpg" width = " 3840" height = " 2719" alt = " " loading = " lazy" /> </ picture> </ div> < div class = " container section" > < div class = " columns" > < div class = " column" > < h3 class = " title" > {{ $t("A practical tool") }}</ h3> < p v-html = " $t( ' Mobilizon is a tool that helps you <b>find, create and organise events</b>.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("An ethical alternative") }}</ h3> < p v-html = " $t( ' Ethical alternative to Facebook events, groups and pages, Mobilizon is a <b>tool designed to serve you</b>. Period.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("A federated software") }}</ h3> < p v-html = " $t( ' Mobilizon is not a giant platform, but a <b>multitude of interconnected Mobilizon websites</b>.' ) " /> </ div> </ div> < div class = " buttons" > < a class = " button is-primary is-large" href = " https://joinmobilizon.org" > {{ $t("Learn more about Mobilizon") }}</ a > </ div> </ div> </ div> < div class = " container section" v-if = " config && loggedUser && loggedUser.settings" > < section v-if = " currentActor.id && (welcomeBack || newRegisteredUser)" > < b-message type = " is-info" v-if = " welcomeBack" > {{ $t("Welcome back {username}!", { username: currentActor.displayName(), }) }}</ b-message> < b-message type = " is-info" v-if = " newRegisteredUser" > {{ $t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName(), }) }}</ b-message> </ section> < section v-if = " canShowMyUpcomingEvents" > < h2 class = " title" > {{ $t("Your upcoming events") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div v-for = " row of goingToEvents" class = " upcoming-events" :key = " row[0]" > < p class = " date-component-container" v-if = " isInLessThanSevenDays(row[0])" > < span v-if = " isToday(row[0])" > {{ $tc("You have one event today.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isTomorrow(row[0])" > {{ $tc("You have one event tomorrow.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isInLessThanSevenDays(row[0])" > {{ $tc("You have one event in {days} days.", row[1].length, { count: row[1].length, days: calculateDiffDays(row[0]), }) }} </ span> </ p> < div> < EventListCard v-for = " participation in thisWeek(row)" @event-deleted = " eventDeleted" :key = " participation[1].id" :participation = " participation[1]" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.MY_EVENTS }" > {{ $t("View everything") }} >></ router-link > </ span> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents && canShowLastWeekEvents" /> < section v-if = " canShowLastWeekEvents" > < h2 class = " title" > {{ $t("Last week") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div> < EventListCard v-for = " participation in lastWeekEvents" :key = " participation.id" :participation = " participation" @event-deleted = " eventDeleted" :options = " { hideDate: false }" /> </ div> </ section> < hr class = " home-separator" v-if = " canShowLastWeekEvents && canShowCloseEvents" /> < section class = " events-close" v-if = " canShowCloseEvents" > < h2 class = " title" > {{ $t("Events nearby") }} </ h2> < p> {{ $tc( "Within {number} kilometers of {place}", loggedUser.settings.location.range, { number: loggedUser.settings.location.range, place: loggedUser.settings.location.name, } ) }} < router-link :to = " { name: RouteName.PREFERENCES }" :title = " $t(' Change' )" > < b-icon class = " clickable" icon = " pencil" size = " is-small" /> </ router-link> < b-loading :active.sync = " $apollo.loading" /> </ p> < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in closeEvents.elements.slice(0, 3)" :key = " event.uuid" > < event-card :event = " event" /> </ div> </ div> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents " /> < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < recent-event-card-wrapper :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}< br /> < div v-if = " goingToEvents.size > 0 || lastWeekEvents.length > 0" > < b-icon size = " is-small" icon = " information-outline" /> < small> {{ $t("The events you created are not shown here.") }}</ small> </ div> </ b-message> </ section> </ div> </ div> </ template> < script lang = " ts" > import { Component, Vue, Watch } from "vue-property-decorator" ; import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums" ; import { Paginate } from "@/types/paginate" ; import { supportsWebPFormat } from "@/utils/support" ; import { IParticipant, Participant } from "../types/participant.model" ; import { CLOSE_EVENTS , FETCH_EVENTS } from "../graphql/event" ; import EventListCard from "../components/Event/EventListCard.vue" ; import EventCard from "../components/Event/EventCard.vue" ; import RecentEventCardWrapper from "../components/Event/RecentEventCardWrapper.vue" ; import { CURRENT_ACTOR_CLIENT , LOGGED_USER_PARTICIPATIONS , } from "../graphql/actor" ; import { IPerson, Person } from "../types/actor" ; import { ICurrentUser, IUser } from "../types/current-user.model" ; import { CURRENT_USER_CLIENT , USER_SETTINGS } from "../graphql/user" ; import RouteName from "../router/name" ; import { IEvent } from "../types/event.model" ; import DateComponent from "../components/Event/DateCalendarIcon.vue" ; import { CONFIG } from "../graphql/config" ; import { IConfig } from "../types/config.model" ; import Subtitle from "../components/Utils/Subtitle.vue" ; @Component ( { apollo: { events: { query: FETCH_EVENTS , fetchPolicy: "no-cache" , variables: { orderBy: EventSortField. INSERTED_AT , direction: SortDirection. DESC , } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , update : ( data ) => new Person ( data. currentActor) , } , currentUser: CURRENT_USER_CLIENT , loggedUser: { query: USER_SETTINGS , fetchPolicy: "network-only" , skip ( ) { return ! this . currentUser || this . currentUser. isLoggedIn === false ; } , error ( ) { return null ; } , } , config: CONFIG , currentUserParticipations: { query: LOGGED_USER_PARTICIPATIONS , fetchPolicy: "cache-and-network" , variables ( ) { const lastWeek = new Date ( ) ; lastWeek. setDate ( new Date ( ) . getDate ( ) - 7 ) ; return { afterDateTime: lastWeek. toISOString ( ) , } ; } , update : ( data ) => data. loggedUser. participations. elements. map ( ( participation: IParticipant ) => new Participant ( participation) ) , skip ( ) { return this . currentUser?. isLoggedIn === false ; } , } , closeEvents: { query: CLOSE_EVENTS , variables ( ) { return { location: this . loggedUser?. settings?. location?. geohash, radius: this . loggedUser?. settings?. location?. range, } ; } , update : ( data ) => data. searchEvents, skip ( ) { return ( ! this . currentUser?. isLoggedIn || ! this . loggedUser?. settings?. location?. geohash || ! this . loggedUser?. settings?. location?. range ) ; } , } , } , components: { Subtitle, DateComponent, EventListCard, EventCard, RecentEventCardWrapper, "settings-onboard" : ( ) => import ( "./User/SettingsOnboard.vue" ) , } , metaInfo ( ) { return { title: this . instanceName, titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class Home extends Vue { events: Paginate< IEvent> = { elements: [ ] , total: 0 , } ; locations = [ ] ; city = { name: null } ; country = { name: null } ; currentUser! : IUser; loggedUser! : ICurrentUser; currentActor! : IPerson; config! : IConfig; RouteName = RouteName; currentUserParticipations: IParticipant[ ] = [ ] ; supportsWebPFormat = supportsWebPFormat; closeEvents: Paginate< IEvent> = { elements: [ ] , total: 0 } ; get instanceName ( ) : string | undefined { if ( ! this . config) return undefined ; return this . config. name; } get welcomeBack ( ) : boolean { return window. localStorage. getItem ( "welcome-back" ) === "yes" ; } get newRegisteredUser ( ) : boolean { return window. localStorage. getItem ( "new-registered-user" ) === "yes" ; } thisWeek ( row: [ string, Map< string, IParticipant> ] ) : Map< string, IParticipant> { if ( this . isInLessThanSevenDays ( row[ 0 ] ) ) { return row[ 1 ] ; } return new Map ( ) ; } mounted ( ) : void { if ( window. localStorage. getItem ( "welcome-back" ) ) { window. localStorage. removeItem ( "welcome-back" ) ; } if ( window. localStorage. getItem ( "new-registered-user" ) ) { window. localStorage. removeItem ( "new-registered-user" ) ; } } isToday ( date: Date) : boolean { return new Date ( date) . toDateString ( ) === new Date ( ) . toDateString ( ) ; } isTomorrow ( date: string) : boolean { return this . isInDays ( date, 1 ) ; } isInDays ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) === nbDays; } isBefore ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) < nbDays; } isAfter ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) >= nbDays; } isInLessThanSevenDays ( date: string) : boolean { return this . isBefore ( date, 7 ) ; } calculateDiffDays ( date: string) : number { return Math. ceil ( ( new Date ( date) . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 / 60 / 60 / 24 ) ; } get thisWeekGoingToEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isAfter ( event. beginsOn. toDateString ( ) , 0 ) && this . isBefore ( event. beginsOn. toDateString ( ) , 7 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } get goingToEvents ( ) : Map< string, Map< string, IParticipant>> { return this . thisWeekGoingToEvents. reduce ( ( acc: Map< string, Map< string, IParticipant>> , participation: IParticipant ) => { const day = new Date ( participation. event. beginsOn) . toDateString ( ) ; const participations: Map< string, IParticipant> = acc. get ( day) || new Map ( ) ; participations. set ( ` ${ participation. event. uuid} ${ participation. actor. id} ` , participation ) ; acc. set ( day, participations) ; return acc; } , new Map ( ) ) ; } get lastWeekEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isBefore ( event. beginsOn. toDateString ( ) , 0 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } eventDeleted ( eventid: string) : void { this . currentUserParticipations = this . currentUserParticipations. filter ( ( participation ) => participation. event. id !== eventid ) ; } viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; } @Watch ( "loggedUser" ) detectEmptyUserSettings ( loggedUser: IUser) : void { if ( loggedUser && loggedUser. id && loggedUser. settings === null ) { this . $router. push ( { name: RouteName. WELCOME_SCREEN , params: { step: "1" } , } ) ; } } get canShowMyUpcomingEvents ( ) : boolean { return this . currentActor. id != undefined && this . goingToEvents. size > 0 ; } get canShowLastWeekEvents ( ) : boolean { return this . currentActor && this . lastWeekEvents. length > 0 ; } get canShowCloseEvents ( ) : boolean { return this . closeEvents. total > 0 ; } } </ script> < style lang = " scss" scoped > @import "~bulma/sass/utilities/mixins.sass" ; main > div > .container { background : $white; padding : 1rem 0.5rem 3rem; } .search-autocomplete { border : 1px solid #dbdbdb; color : rgba ( 0, 0, 0, 0.87) ; } .events-recent { & > h3 { padding-left : 0.75rem; } .columns { margin : 1rem auto 0; } } .date-component-container { display : flex; align-items : center; margin : 0.5rem auto 1rem; h3.subtitle { margin-left : 7px; } } span.view-all { display : block; margin-top : 1rem; text-align : right; a { text-decoration : underline; } } section.hero { position : relative; z-index : 1; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; opacity : 0.3; z-index : -1; background : url ( "../../public/img/pics/homepage_background-1024w.png" ) ; background-size : cover; } &.webp::before { background-image : url ( "../../public/img/pics/homepage_background-1024w.webp" ) ; } & > .hero-body { padding : 1rem 1.5rem 3rem; } .title { color : $background-color; } .column figure.image img { max-width : 400px; } .instance-description { margin-bottom : 1rem; } } #recent_events { padding : 0; min-height : 20vh; z-index : 10; .title { margin : 20px auto 0; } .columns { margin : 0 auto; } } #picture { .picture-container { position : relative; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; z-index : 1; } & > img { object-fit : cover; max-height : 80vh; display : block; margin : auto; width : 100%; } } .container.section { background : $white; @include tablet { margin-top : -4rem; } z-index : 10; .title { margin : 0 0 10px; font-size : 30px; } .buttons { justify-content : center; margin-top : 2rem; } } } #homepage { background : $white; } .home-separator { background-color : $orange-2; } .clickable { cursor : pointer; } .title { font-size : 27px; &:not(:last-child) { margin-bottom : 0.5rem; } } </ style>
GraphQL data fetching using apollo
This @Component
annotation and the apollo
attribute describes the GraphQL queries upon which the component relies on to be rendered.
Data such as the following may be fetched from GraphQL:
events
: A list of Mobilizon Events
currentActor
: Informations about the current actor connected
currentUser
: Informations about the current user connected
loggedUser
: A flag to know if a user is logged-in
config
: Configuration that could be retrieved about the server
currentUserParticipations
: Events in which the current user takes part
closeEvents
: Events that are geographically close to the user's location
Browse code : js/src/views/Home.vue
Reveal code
< template> < div id = " homepage" > < section class = " hero" :class = " { webp: supportsWebPFormat }" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " hero-body" > < div class = " container" > < h1 class = " title" > {{ config.slogan || $t("Gather â‹… Organize â‹… Mobilize") }} </ h1> < p v-html = " $t(' Join <b>{instance}</b>, a Mobilizon instance' , { instance: config.name, }) " /> < p class = " instance-description" > {{ config.description }}</ p> < b-message type = " is-danger" v-if = " !config.registrationsOpen" > {{ $t("Unfortunately, this instance isn't opened to registrations") }}</ b-message> < div class = " buttons" > < b-button type = " is-primary" tag = " router-link" :to = " { name: RouteName.REGISTER }" v-if = " config.registrationsOpen" > {{ $t("Create an account") }}</ b-button > < b-button type = " is-text" tag = " router-link" :to = " { name: RouteName.ABOUT }" > {{ $t("Learn more about {instance}", { instance: config.name }) }} </ b-button> </ div> </ div> </ div> </ section> < div id = " recent_events" class = " container section" v-if = " config && (!currentUser.id || !currentActor.id)" > < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < b-loading :active.sync = " $apollo.loading" /> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < EventCard :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}</ b-message> </ section> </ div> < div id = " picture" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " picture-container" > < picture> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.webp" type = " image/webp" /> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.webp" type = " image/webp" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.webp" type = " image/webp" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.jpg" type = " image/jpeg" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.webp" type = " image/webp" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.jpg" type = " image/jpeg" /> < img src = " /img/pics/homepage-1024w.jpg" width = " 3840" height = " 2719" alt = " " loading = " lazy" /> </ picture> </ div> < div class = " container section" > < div class = " columns" > < div class = " column" > < h3 class = " title" > {{ $t("A practical tool") }}</ h3> < p v-html = " $t( ' Mobilizon is a tool that helps you <b>find, create and organise events</b>.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("An ethical alternative") }}</ h3> < p v-html = " $t( ' Ethical alternative to Facebook events, groups and pages, Mobilizon is a <b>tool designed to serve you</b>. Period.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("A federated software") }}</ h3> < p v-html = " $t( ' Mobilizon is not a giant platform, but a <b>multitude of interconnected Mobilizon websites</b>.' ) " /> </ div> </ div> < div class = " buttons" > < a class = " button is-primary is-large" href = " https://joinmobilizon.org" > {{ $t("Learn more about Mobilizon") }}</ a > </ div> </ div> </ div> < div class = " container section" v-if = " config && loggedUser && loggedUser.settings" > < section v-if = " currentActor.id && (welcomeBack || newRegisteredUser)" > < b-message type = " is-info" v-if = " welcomeBack" > {{ $t("Welcome back {username}!", { username: currentActor.displayName(), }) }}</ b-message> < b-message type = " is-info" v-if = " newRegisteredUser" > {{ $t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName(), }) }}</ b-message> </ section> < section v-if = " canShowMyUpcomingEvents" > < h2 class = " title" > {{ $t("Your upcoming events") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div v-for = " row of goingToEvents" class = " upcoming-events" :key = " row[0]" > < p class = " date-component-container" v-if = " isInLessThanSevenDays(row[0])" > < span v-if = " isToday(row[0])" > {{ $tc("You have one event today.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isTomorrow(row[0])" > {{ $tc("You have one event tomorrow.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isInLessThanSevenDays(row[0])" > {{ $tc("You have one event in {days} days.", row[1].length, { count: row[1].length, days: calculateDiffDays(row[0]), }) }} </ span> </ p> < div> < EventListCard v-for = " participation in thisWeek(row)" @event-deleted = " eventDeleted" :key = " participation[1].id" :participation = " participation[1]" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.MY_EVENTS }" > {{ $t("View everything") }} >></ router-link > </ span> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents && canShowLastWeekEvents" /> < section v-if = " canShowLastWeekEvents" > < h2 class = " title" > {{ $t("Last week") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div> < EventListCard v-for = " participation in lastWeekEvents" :key = " participation.id" :participation = " participation" @event-deleted = " eventDeleted" :options = " { hideDate: false }" /> </ div> </ section> < hr class = " home-separator" v-if = " canShowLastWeekEvents && canShowCloseEvents" /> < section class = " events-close" v-if = " canShowCloseEvents" > < h2 class = " title" > {{ $t("Events nearby") }} </ h2> < p> {{ $tc( "Within {number} kilometers of {place}", loggedUser.settings.location.range, { number: loggedUser.settings.location.range, place: loggedUser.settings.location.name, } ) }} < router-link :to = " { name: RouteName.PREFERENCES }" :title = " $t(' Change' )" > < b-icon class = " clickable" icon = " pencil" size = " is-small" /> </ router-link> < b-loading :active.sync = " $apollo.loading" /> </ p> < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in closeEvents.elements.slice(0, 3)" :key = " event.uuid" > < event-card :event = " event" /> </ div> </ div> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents " /> < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < recent-event-card-wrapper :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}< br /> < div v-if = " goingToEvents.size > 0 || lastWeekEvents.length > 0" > < b-icon size = " is-small" icon = " information-outline" /> < small> {{ $t("The events you created are not shown here.") }}</ small> </ div> </ b-message> </ section> </ div> </ div> </ template> < script lang = " ts" > import { Component, Vue, Watch } from "vue-property-decorator" ; import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums" ; import { Paginate } from "@/types/paginate" ; import { supportsWebPFormat } from "@/utils/support" ; import { IParticipant, Participant } from "../types/participant.model" ; import { CLOSE_EVENTS , FETCH_EVENTS } from "../graphql/event" ; import EventListCard from "../components/Event/EventListCard.vue" ; import EventCard from "../components/Event/EventCard.vue" ; import RecentEventCardWrapper from "../components/Event/RecentEventCardWrapper.vue" ; import { CURRENT_ACTOR_CLIENT , LOGGED_USER_PARTICIPATIONS , } from "../graphql/actor" ; import { IPerson, Person } from "../types/actor" ; import { ICurrentUser, IUser } from "../types/current-user.model" ; import { CURRENT_USER_CLIENT , USER_SETTINGS } from "../graphql/user" ; import RouteName from "../router/name" ; import { IEvent } from "../types/event.model" ; import DateComponent from "../components/Event/DateCalendarIcon.vue" ; import { CONFIG } from "../graphql/config" ; import { IConfig } from "../types/config.model" ; import Subtitle from "../components/Utils/Subtitle.vue" ; @Component ( { apollo: { events: { query: FETCH_EVENTS , fetchPolicy: "no-cache" , variables: { orderBy: EventSortField. INSERTED_AT , direction: SortDirection. DESC , } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , update : ( data ) => new Person ( data. currentActor) , } , currentUser: CURRENT_USER_CLIENT , loggedUser: { query: USER_SETTINGS , fetchPolicy: "network-only" , skip ( ) { return ! this . currentUser || this . currentUser. isLoggedIn === false ; } , error ( ) { return null ; } , } , config: CONFIG , currentUserParticipations: { query: LOGGED_USER_PARTICIPATIONS , fetchPolicy: "cache-and-network" , variables ( ) { const lastWeek = new Date ( ) ; lastWeek. setDate ( new Date ( ) . getDate ( ) - 7 ) ; return { afterDateTime: lastWeek. toISOString ( ) , } ; } , update : ( data ) => data. loggedUser. participations. elements. map ( ( participation: IParticipant ) => new Participant ( participation) ) , skip ( ) { return this . currentUser?. isLoggedIn === false ; } , } , closeEvents: { query: CLOSE_EVENTS , variables ( ) { return { location: this . loggedUser?. settings?. location?. geohash, radius: this . loggedUser?. settings?. location?. range, } ; } , update : ( data ) => data. searchEvents, skip ( ) { return ( ! this . currentUser?. isLoggedIn || ! this . loggedUser?. settings?. location?. geohash || ! this . loggedUser?. settings?. location?. range ) ; } , } , } , components: { Subtitle, DateComponent, EventListCard, EventCard, RecentEventCardWrapper, "settings-onboard" : ( ) => import ( "./User/SettingsOnboard.vue" ) , } , metaInfo ( ) { return { title: this . instanceName, titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class Home extends Vue { events: Paginate< IEvent> = { elements: [ ] , total: 0 , } ; locations = [ ] ; city = { name: null } ; country = { name: null } ; currentUser! : IUser; loggedUser! : ICurrentUser; currentActor! : IPerson; config! : IConfig; RouteName = RouteName; currentUserParticipations: IParticipant[ ] = [ ] ; supportsWebPFormat = supportsWebPFormat; closeEvents: Paginate< IEvent> = { elements: [ ] , total: 0 } ; get instanceName ( ) : string | undefined { if ( ! this . config) return undefined ; return this . config. name; } get welcomeBack ( ) : boolean { return window. localStorage. getItem ( "welcome-back" ) === "yes" ; } get newRegisteredUser ( ) : boolean { return window. localStorage. getItem ( "new-registered-user" ) === "yes" ; } thisWeek ( row: [ string, Map< string, IParticipant> ] ) : Map< string, IParticipant> { if ( this . isInLessThanSevenDays ( row[ 0 ] ) ) { return row[ 1 ] ; } return new Map ( ) ; } mounted ( ) : void { if ( window. localStorage. getItem ( "welcome-back" ) ) { window. localStorage. removeItem ( "welcome-back" ) ; } if ( window. localStorage. getItem ( "new-registered-user" ) ) { window. localStorage. removeItem ( "new-registered-user" ) ; } } isToday ( date: Date) : boolean { return new Date ( date) . toDateString ( ) === new Date ( ) . toDateString ( ) ; } isTomorrow ( date: string) : boolean { return this . isInDays ( date, 1 ) ; } isInDays ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) === nbDays; } isBefore ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) < nbDays; } isAfter ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) >= nbDays; } isInLessThanSevenDays ( date: string) : boolean { return this . isBefore ( date, 7 ) ; } calculateDiffDays ( date: string) : number { return Math. ceil ( ( new Date ( date) . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 / 60 / 60 / 24 ) ; } get thisWeekGoingToEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isAfter ( event. beginsOn. toDateString ( ) , 0 ) && this . isBefore ( event. beginsOn. toDateString ( ) , 7 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } get goingToEvents ( ) : Map< string, Map< string, IParticipant>> { return this . thisWeekGoingToEvents. reduce ( ( acc: Map< string, Map< string, IParticipant>> , participation: IParticipant ) => { const day = new Date ( participation. event. beginsOn) . toDateString ( ) ; const participations: Map< string, IParticipant> = acc. get ( day) || new Map ( ) ; participations. set ( ` ${ participation. event. uuid} ${ participation. actor. id} ` , participation ) ; acc. set ( day, participations) ; return acc; } , new Map ( ) ) ; } get lastWeekEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isBefore ( event. beginsOn. toDateString ( ) , 0 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } eventDeleted ( eventid: string) : void { this . currentUserParticipations = this . currentUserParticipations. filter ( ( participation ) => participation. event. id !== eventid ) ; } viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; } @Watch ( "loggedUser" ) detectEmptyUserSettings ( loggedUser: IUser) : void { if ( loggedUser && loggedUser. id && loggedUser. settings === null ) { this . $router. push ( { name: RouteName. WELCOME_SCREEN , params: { step: "1" } , } ) ; } } get canShowMyUpcomingEvents ( ) : boolean { return this . currentActor. id != undefined && this . goingToEvents. size > 0 ; } get canShowLastWeekEvents ( ) : boolean { return this . currentActor && this . lastWeekEvents. length > 0 ; } get canShowCloseEvents ( ) : boolean { return this . closeEvents. total > 0 ; } } </ script> < style lang = " scss" scoped > @import "~bulma/sass/utilities/mixins.sass" ; main > div > .container { background : $white; padding : 1rem 0.5rem 3rem; } .search-autocomplete { border : 1px solid #dbdbdb; color : rgba ( 0, 0, 0, 0.87) ; } .events-recent { & > h3 { padding-left : 0.75rem; } .columns { margin : 1rem auto 0; } } .date-component-container { display : flex; align-items : center; margin : 0.5rem auto 1rem; h3.subtitle { margin-left : 7px; } } span.view-all { display : block; margin-top : 1rem; text-align : right; a { text-decoration : underline; } } section.hero { position : relative; z-index : 1; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; opacity : 0.3; z-index : -1; background : url ( "../../public/img/pics/homepage_background-1024w.png" ) ; background-size : cover; } &.webp::before { background-image : url ( "../../public/img/pics/homepage_background-1024w.webp" ) ; } & > .hero-body { padding : 1rem 1.5rem 3rem; } .title { color : $background-color; } .column figure.image img { max-width : 400px; } .instance-description { margin-bottom : 1rem; } } #recent_events { padding : 0; min-height : 20vh; z-index : 10; .title { margin : 20px auto 0; } .columns { margin : 0 auto; } } #picture { .picture-container { position : relative; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; z-index : 1; } & > img { object-fit : cover; max-height : 80vh; display : block; margin : auto; width : 100%; } } .container.section { background : $white; @include tablet { margin-top : -4rem; } z-index : 10; .title { margin : 0 0 10px; font-size : 30px; } .buttons { justify-content : center; margin-top : 2rem; } } } #homepage { background : $white; } .home-separator { background-color : $orange-2; } .clickable { cursor : pointer; } .title { font-size : 27px; &:not(:last-child) { margin-bottom : 0.5rem; } } </ style>
get
functions in view templates
Most of the get
prefixed function are referred in the <template></template>
section so most of the computation is done in the <script></script>
part of the file.
In the <template></template
part only view rendering computations are done (conditional rendering, collection pagination, internationalization...).
Browse code : js/src/views/Home.vue
Reveal code
< template> < div id = " homepage" > < section class = " hero" :class = " { webp: supportsWebPFormat }" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " hero-body" > < div class = " container" > < h1 class = " title" > {{ config.slogan || $t("Gather â‹… Organize â‹… Mobilize") }} </ h1> < p v-html = " $t(' Join <b>{instance}</b>, a Mobilizon instance' , { instance: config.name, }) " /> < p class = " instance-description" > {{ config.description }}</ p> < b-message type = " is-danger" v-if = " !config.registrationsOpen" > {{ $t("Unfortunately, this instance isn't opened to registrations") }}</ b-message> < div class = " buttons" > < b-button type = " is-primary" tag = " router-link" :to = " { name: RouteName.REGISTER }" v-if = " config.registrationsOpen" > {{ $t("Create an account") }}</ b-button > < b-button type = " is-text" tag = " router-link" :to = " { name: RouteName.ABOUT }" > {{ $t("Learn more about {instance}", { instance: config.name }) }} </ b-button> </ div> </ div> </ div> </ section> < div id = " recent_events" class = " container section" v-if = " config && (!currentUser.id || !currentActor.id)" > < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < b-loading :active.sync = " $apollo.loading" /> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < EventCard :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}</ b-message> </ section> </ div> < div id = " picture" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " picture-container" > < picture> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.webp" type = " image/webp" /> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.webp" type = " image/webp" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.webp" type = " image/webp" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.jpg" type = " image/jpeg" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.webp" type = " image/webp" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.jpg" type = " image/jpeg" /> < img src = " /img/pics/homepage-1024w.jpg" width = " 3840" height = " 2719" alt = " " loading = " lazy" /> </ picture> </ div> < div class = " container section" > < div class = " columns" > < div class = " column" > < h3 class = " title" > {{ $t("A practical tool") }}</ h3> < p v-html = " $t( ' Mobilizon is a tool that helps you <b>find, create and organise events</b>.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("An ethical alternative") }}</ h3> < p v-html = " $t( ' Ethical alternative to Facebook events, groups and pages, Mobilizon is a <b>tool designed to serve you</b>. Period.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("A federated software") }}</ h3> < p v-html = " $t( ' Mobilizon is not a giant platform, but a <b>multitude of interconnected Mobilizon websites</b>.' ) " /> </ div> </ div> < div class = " buttons" > < a class = " button is-primary is-large" href = " https://joinmobilizon.org" > {{ $t("Learn more about Mobilizon") }}</ a > </ div> </ div> </ div> < div class = " container section" v-if = " config && loggedUser && loggedUser.settings" > < section v-if = " currentActor.id && (welcomeBack || newRegisteredUser)" > < b-message type = " is-info" v-if = " welcomeBack" > {{ $t("Welcome back {username}!", { username: currentActor.displayName(), }) }}</ b-message> < b-message type = " is-info" v-if = " newRegisteredUser" > {{ $t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName(), }) }}</ b-message> </ section> < section v-if = " canShowMyUpcomingEvents" > < h2 class = " title" > {{ $t("Your upcoming events") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div v-for = " row of goingToEvents" class = " upcoming-events" :key = " row[0]" > < p class = " date-component-container" v-if = " isInLessThanSevenDays(row[0])" > < span v-if = " isToday(row[0])" > {{ $tc("You have one event today.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isTomorrow(row[0])" > {{ $tc("You have one event tomorrow.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isInLessThanSevenDays(row[0])" > {{ $tc("You have one event in {days} days.", row[1].length, { count: row[1].length, days: calculateDiffDays(row[0]), }) }} </ span> </ p> < div> < EventListCard v-for = " participation in thisWeek(row)" @event-deleted = " eventDeleted" :key = " participation[1].id" :participation = " participation[1]" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.MY_EVENTS }" > {{ $t("View everything") }} >></ router-link > </ span> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents && canShowLastWeekEvents" /> < section v-if = " canShowLastWeekEvents" > < h2 class = " title" > {{ $t("Last week") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div> < EventListCard v-for = " participation in lastWeekEvents" :key = " participation.id" :participation = " participation" @event-deleted = " eventDeleted" :options = " { hideDate: false }" /> </ div> </ section> < hr class = " home-separator" v-if = " canShowLastWeekEvents && canShowCloseEvents" /> < section class = " events-close" v-if = " canShowCloseEvents" > < h2 class = " title" > {{ $t("Events nearby") }} </ h2> < p> {{ $tc( "Within {number} kilometers of {place}", loggedUser.settings.location.range, { number: loggedUser.settings.location.range, place: loggedUser.settings.location.name, } ) }} < router-link :to = " { name: RouteName.PREFERENCES }" :title = " $t(' Change' )" > < b-icon class = " clickable" icon = " pencil" size = " is-small" /> </ router-link> < b-loading :active.sync = " $apollo.loading" /> </ p> < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in closeEvents.elements.slice(0, 3)" :key = " event.uuid" > < event-card :event = " event" /> </ div> </ div> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents " /> < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < recent-event-card-wrapper :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}< br /> < div v-if = " goingToEvents.size > 0 || lastWeekEvents.length > 0" > < b-icon size = " is-small" icon = " information-outline" /> < small> {{ $t("The events you created are not shown here.") }}</ small> </ div> </ b-message> </ section> </ div> </ div> </ template> < script lang = " ts" > import { Component, Vue, Watch } from "vue-property-decorator" ; import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums" ; import { Paginate } from "@/types/paginate" ; import { supportsWebPFormat } from "@/utils/support" ; import { IParticipant, Participant } from "../types/participant.model" ; import { CLOSE_EVENTS , FETCH_EVENTS } from "../graphql/event" ; import EventListCard from "../components/Event/EventListCard.vue" ; import EventCard from "../components/Event/EventCard.vue" ; import RecentEventCardWrapper from "../components/Event/RecentEventCardWrapper.vue" ; import { CURRENT_ACTOR_CLIENT , LOGGED_USER_PARTICIPATIONS , } from "../graphql/actor" ; import { IPerson, Person } from "../types/actor" ; import { ICurrentUser, IUser } from "../types/current-user.model" ; import { CURRENT_USER_CLIENT , USER_SETTINGS } from "../graphql/user" ; import RouteName from "../router/name" ; import { IEvent } from "../types/event.model" ; import DateComponent from "../components/Event/DateCalendarIcon.vue" ; import { CONFIG } from "../graphql/config" ; import { IConfig } from "../types/config.model" ; import Subtitle from "../components/Utils/Subtitle.vue" ; @Component ( { apollo: { events: { query: FETCH_EVENTS , fetchPolicy: "no-cache" , variables: { orderBy: EventSortField. INSERTED_AT , direction: SortDirection. DESC , } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , update : ( data ) => new Person ( data. currentActor) , } , currentUser: CURRENT_USER_CLIENT , loggedUser: { query: USER_SETTINGS , fetchPolicy: "network-only" , skip ( ) { return ! this . currentUser || this . currentUser. isLoggedIn === false ; } , error ( ) { return null ; } , } , config: CONFIG , currentUserParticipations: { query: LOGGED_USER_PARTICIPATIONS , fetchPolicy: "cache-and-network" , variables ( ) { const lastWeek = new Date ( ) ; lastWeek. setDate ( new Date ( ) . getDate ( ) - 7 ) ; return { afterDateTime: lastWeek. toISOString ( ) , } ; } , update : ( data ) => data. loggedUser. participations. elements. map ( ( participation: IParticipant ) => new Participant ( participation) ) , skip ( ) { return this . currentUser?. isLoggedIn === false ; } , } , closeEvents: { query: CLOSE_EVENTS , variables ( ) { return { location: this . loggedUser?. settings?. location?. geohash, radius: this . loggedUser?. settings?. location?. range, } ; } , update : ( data ) => data. searchEvents, skip ( ) { return ( ! this . currentUser?. isLoggedIn || ! this . loggedUser?. settings?. location?. geohash || ! this . loggedUser?. settings?. location?. range ) ; } , } , } , components: { Subtitle, DateComponent, EventListCard, EventCard, RecentEventCardWrapper, "settings-onboard" : ( ) => import ( "./User/SettingsOnboard.vue" ) , } , metaInfo ( ) { return { title: this . instanceName, titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class Home extends Vue { events: Paginate< IEvent> = { elements: [ ] , total: 0 , } ; locations = [ ] ; city = { name: null } ; country = { name: null } ; currentUser! : IUser; loggedUser! : ICurrentUser; currentActor! : IPerson; config! : IConfig; RouteName = RouteName; currentUserParticipations: IParticipant[ ] = [ ] ; supportsWebPFormat = supportsWebPFormat; closeEvents: Paginate< IEvent> = { elements: [ ] , total: 0 } ; get instanceName ( ) : string | undefined { if ( ! this . config) return undefined ; return this . config. name; } get welcomeBack ( ) : boolean { return window. localStorage. getItem ( "welcome-back" ) === "yes" ; } get newRegisteredUser ( ) : boolean { return window. localStorage. getItem ( "new-registered-user" ) === "yes" ; } thisWeek ( row: [ string, Map< string, IParticipant> ] ) : Map< string, IParticipant> { if ( this . isInLessThanSevenDays ( row[ 0 ] ) ) { return row[ 1 ] ; } return new Map ( ) ; } mounted ( ) : void { if ( window. localStorage. getItem ( "welcome-back" ) ) { window. localStorage. removeItem ( "welcome-back" ) ; } if ( window. localStorage. getItem ( "new-registered-user" ) ) { window. localStorage. removeItem ( "new-registered-user" ) ; } } isToday ( date: Date) : boolean { return new Date ( date) . toDateString ( ) === new Date ( ) . toDateString ( ) ; } isTomorrow ( date: string) : boolean { return this . isInDays ( date, 1 ) ; } isInDays ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) === nbDays; } isBefore ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) < nbDays; } isAfter ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) >= nbDays; } isInLessThanSevenDays ( date: string) : boolean { return this . isBefore ( date, 7 ) ; } calculateDiffDays ( date: string) : number { return Math. ceil ( ( new Date ( date) . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 / 60 / 60 / 24 ) ; } get thisWeekGoingToEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isAfter ( event. beginsOn. toDateString ( ) , 0 ) && this . isBefore ( event. beginsOn. toDateString ( ) , 7 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } get goingToEvents ( ) : Map< string, Map< string, IParticipant>> { return this . thisWeekGoingToEvents. reduce ( ( acc: Map< string, Map< string, IParticipant>> , participation: IParticipant ) => { const day = new Date ( participation. event. beginsOn) . toDateString ( ) ; const participations: Map< string, IParticipant> = acc. get ( day) || new Map ( ) ; participations. set ( ` ${ participation. event. uuid} ${ participation. actor. id} ` , participation ) ; acc. set ( day, participations) ; return acc; } , new Map ( ) ) ; } get lastWeekEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isBefore ( event. beginsOn. toDateString ( ) , 0 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } eventDeleted ( eventid: string) : void { this . currentUserParticipations = this . currentUserParticipations. filter ( ( participation ) => participation. event. id !== eventid ) ; } viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; } @Watch ( "loggedUser" ) detectEmptyUserSettings ( loggedUser: IUser) : void { if ( loggedUser && loggedUser. id && loggedUser. settings === null ) { this . $router. push ( { name: RouteName. WELCOME_SCREEN , params: { step: "1" } , } ) ; } } get canShowMyUpcomingEvents ( ) : boolean { return this . currentActor. id != undefined && this . goingToEvents. size > 0 ; } get canShowLastWeekEvents ( ) : boolean { return this . currentActor && this . lastWeekEvents. length > 0 ; } get canShowCloseEvents ( ) : boolean { return this . closeEvents. total > 0 ; } } </ script> < style lang = " scss" scoped > @import "~bulma/sass/utilities/mixins.sass" ; main > div > .container { background : $white; padding : 1rem 0.5rem 3rem; } .search-autocomplete { border : 1px solid #dbdbdb; color : rgba ( 0, 0, 0, 0.87) ; } .events-recent { & > h3 { padding-left : 0.75rem; } .columns { margin : 1rem auto 0; } } .date-component-container { display : flex; align-items : center; margin : 0.5rem auto 1rem; h3.subtitle { margin-left : 7px; } } span.view-all { display : block; margin-top : 1rem; text-align : right; a { text-decoration : underline; } } section.hero { position : relative; z-index : 1; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; opacity : 0.3; z-index : -1; background : url ( "../../public/img/pics/homepage_background-1024w.png" ) ; background-size : cover; } &.webp::before { background-image : url ( "../../public/img/pics/homepage_background-1024w.webp" ) ; } & > .hero-body { padding : 1rem 1.5rem 3rem; } .title { color : $background-color; } .column figure.image img { max-width : 400px; } .instance-description { margin-bottom : 1rem; } } #recent_events { padding : 0; min-height : 20vh; z-index : 10; .title { margin : 20px auto 0; } .columns { margin : 0 auto; } } #picture { .picture-container { position : relative; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; z-index : 1; } & > img { object-fit : cover; max-height : 80vh; display : block; margin : auto; width : 100%; } } .container.section { background : $white; @include tablet { margin-top : -4rem; } z-index : 10; .title { margin : 0 0 10px; font-size : 30px; } .buttons { justify-content : center; margin-top : 2rem; } } } #homepage { background : $white; } .home-separator { background-color : $orange-2; } .clickable { cursor : pointer; } .title { font-size : 27px; &:not(:last-child) { margin-bottom : 0.5rem; } } </ style>
I18n internationalized string 1/2
Depending on the current language, "Last published events"
will be replaced with another text.
Ex. If language is french, the string will be looked up in the i18n/fr_FR.json
file.
{ { $t ( "Last published events" ) } }
Abstract of fr_FR.json
.
{ "Last published events" : "Derniers événements publiés" , }
In this case the french translation will be : "Derniers événements publiés".
Browse code : js/src/views/Home.vue
Reveal code
< template> < div id = " homepage" > < section class = " hero" :class = " { webp: supportsWebPFormat }" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " hero-body" > < div class = " container" > < h1 class = " title" > {{ config.slogan || $t("Gather â‹… Organize â‹… Mobilize") }} </ h1> < p v-html = " $t(' Join <b>{instance}</b>, a Mobilizon instance' , { instance: config.name, }) " /> < p class = " instance-description" > {{ config.description }}</ p> < b-message type = " is-danger" v-if = " !config.registrationsOpen" > {{ $t("Unfortunately, this instance isn't opened to registrations") }}</ b-message> < div class = " buttons" > < b-button type = " is-primary" tag = " router-link" :to = " { name: RouteName.REGISTER }" v-if = " config.registrationsOpen" > {{ $t("Create an account") }}</ b-button > < b-button type = " is-text" tag = " router-link" :to = " { name: RouteName.ABOUT }" > {{ $t("Learn more about {instance}", { instance: config.name }) }} </ b-button> </ div> </ div> </ div> </ section> < div id = " recent_events" class = " container section" v-if = " config && (!currentUser.id || !currentActor.id)" > < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < b-loading :active.sync = " $apollo.loading" /> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < EventCard :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}</ b-message> </ section> </ div> < div id = " picture" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " picture-container" > < picture> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.webp" type = " image/webp" /> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.webp" type = " image/webp" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.webp" type = " image/webp" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.jpg" type = " image/jpeg" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.webp" type = " image/webp" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.jpg" type = " image/jpeg" /> < img src = " /img/pics/homepage-1024w.jpg" width = " 3840" height = " 2719" alt = " " loading = " lazy" /> </ picture> </ div> < div class = " container section" > < div class = " columns" > < div class = " column" > < h3 class = " title" > {{ $t("A practical tool") }}</ h3> < p v-html = " $t( ' Mobilizon is a tool that helps you <b>find, create and organise events</b>.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("An ethical alternative") }}</ h3> < p v-html = " $t( ' Ethical alternative to Facebook events, groups and pages, Mobilizon is a <b>tool designed to serve you</b>. Period.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("A federated software") }}</ h3> < p v-html = " $t( ' Mobilizon is not a giant platform, but a <b>multitude of interconnected Mobilizon websites</b>.' ) " /> </ div> </ div> < div class = " buttons" > < a class = " button is-primary is-large" href = " https://joinmobilizon.org" > {{ $t("Learn more about Mobilizon") }}</ a > </ div> </ div> </ div> < div class = " container section" v-if = " config && loggedUser && loggedUser.settings" > < section v-if = " currentActor.id && (welcomeBack || newRegisteredUser)" > < b-message type = " is-info" v-if = " welcomeBack" > {{ $t("Welcome back {username}!", { username: currentActor.displayName(), }) }}</ b-message> < b-message type = " is-info" v-if = " newRegisteredUser" > {{ $t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName(), }) }}</ b-message> </ section> < section v-if = " canShowMyUpcomingEvents" > < h2 class = " title" > {{ $t("Your upcoming events") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div v-for = " row of goingToEvents" class = " upcoming-events" :key = " row[0]" > < p class = " date-component-container" v-if = " isInLessThanSevenDays(row[0])" > < span v-if = " isToday(row[0])" > {{ $tc("You have one event today.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isTomorrow(row[0])" > {{ $tc("You have one event tomorrow.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isInLessThanSevenDays(row[0])" > {{ $tc("You have one event in {days} days.", row[1].length, { count: row[1].length, days: calculateDiffDays(row[0]), }) }} </ span> </ p> < div> < EventListCard v-for = " participation in thisWeek(row)" @event-deleted = " eventDeleted" :key = " participation[1].id" :participation = " participation[1]" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.MY_EVENTS }" > {{ $t("View everything") }} >></ router-link > </ span> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents && canShowLastWeekEvents" /> < section v-if = " canShowLastWeekEvents" > < h2 class = " title" > {{ $t("Last week") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div> < EventListCard v-for = " participation in lastWeekEvents" :key = " participation.id" :participation = " participation" @event-deleted = " eventDeleted" :options = " { hideDate: false }" /> </ div> </ section> < hr class = " home-separator" v-if = " canShowLastWeekEvents && canShowCloseEvents" /> < section class = " events-close" v-if = " canShowCloseEvents" > < h2 class = " title" > {{ $t("Events nearby") }} </ h2> < p> {{ $tc( "Within {number} kilometers of {place}", loggedUser.settings.location.range, { number: loggedUser.settings.location.range, place: loggedUser.settings.location.name, } ) }} < router-link :to = " { name: RouteName.PREFERENCES }" :title = " $t(' Change' )" > < b-icon class = " clickable" icon = " pencil" size = " is-small" /> </ router-link> < b-loading :active.sync = " $apollo.loading" /> </ p> < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in closeEvents.elements.slice(0, 3)" :key = " event.uuid" > < event-card :event = " event" /> </ div> </ div> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents " /> < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < recent-event-card-wrapper :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}< br /> < div v-if = " goingToEvents.size > 0 || lastWeekEvents.length > 0" > < b-icon size = " is-small" icon = " information-outline" /> < small> {{ $t("The events you created are not shown here.") }}</ small> </ div> </ b-message> </ section> </ div> </ div> </ template> < script lang = " ts" > import { Component, Vue, Watch } from "vue-property-decorator" ; import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums" ; import { Paginate } from "@/types/paginate" ; import { supportsWebPFormat } from "@/utils/support" ; import { IParticipant, Participant } from "../types/participant.model" ; import { CLOSE_EVENTS , FETCH_EVENTS } from "../graphql/event" ; import EventListCard from "../components/Event/EventListCard.vue" ; import EventCard from "../components/Event/EventCard.vue" ; import RecentEventCardWrapper from "../components/Event/RecentEventCardWrapper.vue" ; import { CURRENT_ACTOR_CLIENT , LOGGED_USER_PARTICIPATIONS , } from "../graphql/actor" ; import { IPerson, Person } from "../types/actor" ; import { ICurrentUser, IUser } from "../types/current-user.model" ; import { CURRENT_USER_CLIENT , USER_SETTINGS } from "../graphql/user" ; import RouteName from "../router/name" ; import { IEvent } from "../types/event.model" ; import DateComponent from "../components/Event/DateCalendarIcon.vue" ; import { CONFIG } from "../graphql/config" ; import { IConfig } from "../types/config.model" ; import Subtitle from "../components/Utils/Subtitle.vue" ; @Component ( { apollo: { events: { query: FETCH_EVENTS , fetchPolicy: "no-cache" , variables: { orderBy: EventSortField. INSERTED_AT , direction: SortDirection. DESC , } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , update : ( data ) => new Person ( data. currentActor) , } , currentUser: CURRENT_USER_CLIENT , loggedUser: { query: USER_SETTINGS , fetchPolicy: "network-only" , skip ( ) { return ! this . currentUser || this . currentUser. isLoggedIn === false ; } , error ( ) { return null ; } , } , config: CONFIG , currentUserParticipations: { query: LOGGED_USER_PARTICIPATIONS , fetchPolicy: "cache-and-network" , variables ( ) { const lastWeek = new Date ( ) ; lastWeek. setDate ( new Date ( ) . getDate ( ) - 7 ) ; return { afterDateTime: lastWeek. toISOString ( ) , } ; } , update : ( data ) => data. loggedUser. participations. elements. map ( ( participation: IParticipant ) => new Participant ( participation) ) , skip ( ) { return this . currentUser?. isLoggedIn === false ; } , } , closeEvents: { query: CLOSE_EVENTS , variables ( ) { return { location: this . loggedUser?. settings?. location?. geohash, radius: this . loggedUser?. settings?. location?. range, } ; } , update : ( data ) => data. searchEvents, skip ( ) { return ( ! this . currentUser?. isLoggedIn || ! this . loggedUser?. settings?. location?. geohash || ! this . loggedUser?. settings?. location?. range ) ; } , } , } , components: { Subtitle, DateComponent, EventListCard, EventCard, RecentEventCardWrapper, "settings-onboard" : ( ) => import ( "./User/SettingsOnboard.vue" ) , } , metaInfo ( ) { return { title: this . instanceName, titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class Home extends Vue { events: Paginate< IEvent> = { elements: [ ] , total: 0 , } ; locations = [ ] ; city = { name: null } ; country = { name: null } ; currentUser! : IUser; loggedUser! : ICurrentUser; currentActor! : IPerson; config! : IConfig; RouteName = RouteName; currentUserParticipations: IParticipant[ ] = [ ] ; supportsWebPFormat = supportsWebPFormat; closeEvents: Paginate< IEvent> = { elements: [ ] , total: 0 } ; get instanceName ( ) : string | undefined { if ( ! this . config) return undefined ; return this . config. name; } get welcomeBack ( ) : boolean { return window. localStorage. getItem ( "welcome-back" ) === "yes" ; } get newRegisteredUser ( ) : boolean { return window. localStorage. getItem ( "new-registered-user" ) === "yes" ; } thisWeek ( row: [ string, Map< string, IParticipant> ] ) : Map< string, IParticipant> { if ( this . isInLessThanSevenDays ( row[ 0 ] ) ) { return row[ 1 ] ; } return new Map ( ) ; } mounted ( ) : void { if ( window. localStorage. getItem ( "welcome-back" ) ) { window. localStorage. removeItem ( "welcome-back" ) ; } if ( window. localStorage. getItem ( "new-registered-user" ) ) { window. localStorage. removeItem ( "new-registered-user" ) ; } } isToday ( date: Date) : boolean { return new Date ( date) . toDateString ( ) === new Date ( ) . toDateString ( ) ; } isTomorrow ( date: string) : boolean { return this . isInDays ( date, 1 ) ; } isInDays ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) === nbDays; } isBefore ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) < nbDays; } isAfter ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) >= nbDays; } isInLessThanSevenDays ( date: string) : boolean { return this . isBefore ( date, 7 ) ; } calculateDiffDays ( date: string) : number { return Math. ceil ( ( new Date ( date) . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 / 60 / 60 / 24 ) ; } get thisWeekGoingToEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isAfter ( event. beginsOn. toDateString ( ) , 0 ) && this . isBefore ( event. beginsOn. toDateString ( ) , 7 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } get goingToEvents ( ) : Map< string, Map< string, IParticipant>> { return this . thisWeekGoingToEvents. reduce ( ( acc: Map< string, Map< string, IParticipant>> , participation: IParticipant ) => { const day = new Date ( participation. event. beginsOn) . toDateString ( ) ; const participations: Map< string, IParticipant> = acc. get ( day) || new Map ( ) ; participations. set ( ` ${ participation. event. uuid} ${ participation. actor. id} ` , participation ) ; acc. set ( day, participations) ; return acc; } , new Map ( ) ) ; } get lastWeekEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isBefore ( event. beginsOn. toDateString ( ) , 0 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } eventDeleted ( eventid: string) : void { this . currentUserParticipations = this . currentUserParticipations. filter ( ( participation ) => participation. event. id !== eventid ) ; } viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; } @Watch ( "loggedUser" ) detectEmptyUserSettings ( loggedUser: IUser) : void { if ( loggedUser && loggedUser. id && loggedUser. settings === null ) { this . $router. push ( { name: RouteName. WELCOME_SCREEN , params: { step: "1" } , } ) ; } } get canShowMyUpcomingEvents ( ) : boolean { return this . currentActor. id != undefined && this . goingToEvents. size > 0 ; } get canShowLastWeekEvents ( ) : boolean { return this . currentActor && this . lastWeekEvents. length > 0 ; } get canShowCloseEvents ( ) : boolean { return this . closeEvents. total > 0 ; } } </ script> < style lang = " scss" scoped > @import "~bulma/sass/utilities/mixins.sass" ; main > div > .container { background : $white; padding : 1rem 0.5rem 3rem; } .search-autocomplete { border : 1px solid #dbdbdb; color : rgba ( 0, 0, 0, 0.87) ; } .events-recent { & > h3 { padding-left : 0.75rem; } .columns { margin : 1rem auto 0; } } .date-component-container { display : flex; align-items : center; margin : 0.5rem auto 1rem; h3.subtitle { margin-left : 7px; } } span.view-all { display : block; margin-top : 1rem; text-align : right; a { text-decoration : underline; } } section.hero { position : relative; z-index : 1; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; opacity : 0.3; z-index : -1; background : url ( "../../public/img/pics/homepage_background-1024w.png" ) ; background-size : cover; } &.webp::before { background-image : url ( "../../public/img/pics/homepage_background-1024w.webp" ) ; } & > .hero-body { padding : 1rem 1.5rem 3rem; } .title { color : $background-color; } .column figure.image img { max-width : 400px; } .instance-description { margin-bottom : 1rem; } } #recent_events { padding : 0; min-height : 20vh; z-index : 10; .title { margin : 20px auto 0; } .columns { margin : 0 auto; } } #picture { .picture-container { position : relative; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; z-index : 1; } & > img { object-fit : cover; max-height : 80vh; display : block; margin : auto; width : 100%; } } .container.section { background : $white; @include tablet { margin-top : -4rem; } z-index : 10; .title { margin : 0 0 10px; font-size : 30px; } .buttons { justify-content : center; margin-top : 2rem; } } } #homepage { background : $white; } .home-separator { background-color : $orange-2; } .clickable { cursor : pointer; } .title { font-size : 27px; &:not(:last-child) { margin-bottom : 0.5rem; } } </ style>
I18n internationalized string 2/2
Another way to translate text is by using <i18n></i18n>
element. I allows more complex text substitutions like parametrized text.
Browse code : js/src/views/Home.vue
Reveal code
< template> < div id = " homepage" > < section class = " hero" :class = " { webp: supportsWebPFormat }" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " hero-body" > < div class = " container" > < h1 class = " title" > {{ config.slogan || $t("Gather â‹… Organize â‹… Mobilize") }} </ h1> < p v-html = " $t(' Join <b>{instance}</b>, a Mobilizon instance' , { instance: config.name, }) " /> < p class = " instance-description" > {{ config.description }}</ p> < b-message type = " is-danger" v-if = " !config.registrationsOpen" > {{ $t("Unfortunately, this instance isn't opened to registrations") }}</ b-message> < div class = " buttons" > < b-button type = " is-primary" tag = " router-link" :to = " { name: RouteName.REGISTER }" v-if = " config.registrationsOpen" > {{ $t("Create an account") }}</ b-button > < b-button type = " is-text" tag = " router-link" :to = " { name: RouteName.ABOUT }" > {{ $t("Learn more about {instance}", { instance: config.name }) }} </ b-button> </ div> </ div> </ div> </ section> < div id = " recent_events" class = " container section" v-if = " config && (!currentUser.id || !currentActor.id)" > < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < b-loading :active.sync = " $apollo.loading" /> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < EventCard :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}</ b-message> </ section> </ div> < div id = " picture" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " picture-container" > < picture> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.webp" type = " image/webp" /> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.webp" type = " image/webp" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.webp" type = " image/webp" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.jpg" type = " image/jpeg" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.webp" type = " image/webp" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.jpg" type = " image/jpeg" /> < img src = " /img/pics/homepage-1024w.jpg" width = " 3840" height = " 2719" alt = " " loading = " lazy" /> </ picture> </ div> < div class = " container section" > < div class = " columns" > < div class = " column" > < h3 class = " title" > {{ $t("A practical tool") }}</ h3> < p v-html = " $t( ' Mobilizon is a tool that helps you <b>find, create and organise events</b>.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("An ethical alternative") }}</ h3> < p v-html = " $t( ' Ethical alternative to Facebook events, groups and pages, Mobilizon is a <b>tool designed to serve you</b>. Period.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("A federated software") }}</ h3> < p v-html = " $t( ' Mobilizon is not a giant platform, but a <b>multitude of interconnected Mobilizon websites</b>.' ) " /> </ div> </ div> < div class = " buttons" > < a class = " button is-primary is-large" href = " https://joinmobilizon.org" > {{ $t("Learn more about Mobilizon") }}</ a > </ div> </ div> </ div> < div class = " container section" v-if = " config && loggedUser && loggedUser.settings" > < section v-if = " currentActor.id && (welcomeBack || newRegisteredUser)" > < b-message type = " is-info" v-if = " welcomeBack" > {{ $t("Welcome back {username}!", { username: currentActor.displayName(), }) }}</ b-message> < b-message type = " is-info" v-if = " newRegisteredUser" > {{ $t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName(), }) }}</ b-message> </ section> < section v-if = " canShowMyUpcomingEvents" > < h2 class = " title" > {{ $t("Your upcoming events") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div v-for = " row of goingToEvents" class = " upcoming-events" :key = " row[0]" > < p class = " date-component-container" v-if = " isInLessThanSevenDays(row[0])" > < span v-if = " isToday(row[0])" > {{ $tc("You have one event today.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isTomorrow(row[0])" > {{ $tc("You have one event tomorrow.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isInLessThanSevenDays(row[0])" > {{ $tc("You have one event in {days} days.", row[1].length, { count: row[1].length, days: calculateDiffDays(row[0]), }) }} </ span> </ p> < div> < EventListCard v-for = " participation in thisWeek(row)" @event-deleted = " eventDeleted" :key = " participation[1].id" :participation = " participation[1]" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.MY_EVENTS }" > {{ $t("View everything") }} >></ router-link > </ span> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents && canShowLastWeekEvents" /> < section v-if = " canShowLastWeekEvents" > < h2 class = " title" > {{ $t("Last week") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div> < EventListCard v-for = " participation in lastWeekEvents" :key = " participation.id" :participation = " participation" @event-deleted = " eventDeleted" :options = " { hideDate: false }" /> </ div> </ section> < hr class = " home-separator" v-if = " canShowLastWeekEvents && canShowCloseEvents" /> < section class = " events-close" v-if = " canShowCloseEvents" > < h2 class = " title" > {{ $t("Events nearby") }} </ h2> < p> {{ $tc( "Within {number} kilometers of {place}", loggedUser.settings.location.range, { number: loggedUser.settings.location.range, place: loggedUser.settings.location.name, } ) }} < router-link :to = " { name: RouteName.PREFERENCES }" :title = " $t(' Change' )" > < b-icon class = " clickable" icon = " pencil" size = " is-small" /> </ router-link> < b-loading :active.sync = " $apollo.loading" /> </ p> < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in closeEvents.elements.slice(0, 3)" :key = " event.uuid" > < event-card :event = " event" /> </ div> </ div> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents " /> < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < recent-event-card-wrapper :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}< br /> < div v-if = " goingToEvents.size > 0 || lastWeekEvents.length > 0" > < b-icon size = " is-small" icon = " information-outline" /> < small> {{ $t("The events you created are not shown here.") }}</ small> </ div> </ b-message> </ section> </ div> </ div> </ template> < script lang = " ts" > import { Component, Vue, Watch } from "vue-property-decorator" ; import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums" ; import { Paginate } from "@/types/paginate" ; import { supportsWebPFormat } from "@/utils/support" ; import { IParticipant, Participant } from "../types/participant.model" ; import { CLOSE_EVENTS , FETCH_EVENTS } from "../graphql/event" ; import EventListCard from "../components/Event/EventListCard.vue" ; import EventCard from "../components/Event/EventCard.vue" ; import RecentEventCardWrapper from "../components/Event/RecentEventCardWrapper.vue" ; import { CURRENT_ACTOR_CLIENT , LOGGED_USER_PARTICIPATIONS , } from "../graphql/actor" ; import { IPerson, Person } from "../types/actor" ; import { ICurrentUser, IUser } from "../types/current-user.model" ; import { CURRENT_USER_CLIENT , USER_SETTINGS } from "../graphql/user" ; import RouteName from "../router/name" ; import { IEvent } from "../types/event.model" ; import DateComponent from "../components/Event/DateCalendarIcon.vue" ; import { CONFIG } from "../graphql/config" ; import { IConfig } from "../types/config.model" ; import Subtitle from "../components/Utils/Subtitle.vue" ; @Component ( { apollo: { events: { query: FETCH_EVENTS , fetchPolicy: "no-cache" , variables: { orderBy: EventSortField. INSERTED_AT , direction: SortDirection. DESC , } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , update : ( data ) => new Person ( data. currentActor) , } , currentUser: CURRENT_USER_CLIENT , loggedUser: { query: USER_SETTINGS , fetchPolicy: "network-only" , skip ( ) { return ! this . currentUser || this . currentUser. isLoggedIn === false ; } , error ( ) { return null ; } , } , config: CONFIG , currentUserParticipations: { query: LOGGED_USER_PARTICIPATIONS , fetchPolicy: "cache-and-network" , variables ( ) { const lastWeek = new Date ( ) ; lastWeek. setDate ( new Date ( ) . getDate ( ) - 7 ) ; return { afterDateTime: lastWeek. toISOString ( ) , } ; } , update : ( data ) => data. loggedUser. participations. elements. map ( ( participation: IParticipant ) => new Participant ( participation) ) , skip ( ) { return this . currentUser?. isLoggedIn === false ; } , } , closeEvents: { query: CLOSE_EVENTS , variables ( ) { return { location: this . loggedUser?. settings?. location?. geohash, radius: this . loggedUser?. settings?. location?. range, } ; } , update : ( data ) => data. searchEvents, skip ( ) { return ( ! this . currentUser?. isLoggedIn || ! this . loggedUser?. settings?. location?. geohash || ! this . loggedUser?. settings?. location?. range ) ; } , } , } , components: { Subtitle, DateComponent, EventListCard, EventCard, RecentEventCardWrapper, "settings-onboard" : ( ) => import ( "./User/SettingsOnboard.vue" ) , } , metaInfo ( ) { return { title: this . instanceName, titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class Home extends Vue { events: Paginate< IEvent> = { elements: [ ] , total: 0 , } ; locations = [ ] ; city = { name: null } ; country = { name: null } ; currentUser! : IUser; loggedUser! : ICurrentUser; currentActor! : IPerson; config! : IConfig; RouteName = RouteName; currentUserParticipations: IParticipant[ ] = [ ] ; supportsWebPFormat = supportsWebPFormat; closeEvents: Paginate< IEvent> = { elements: [ ] , total: 0 } ; get instanceName ( ) : string | undefined { if ( ! this . config) return undefined ; return this . config. name; } get welcomeBack ( ) : boolean { return window. localStorage. getItem ( "welcome-back" ) === "yes" ; } get newRegisteredUser ( ) : boolean { return window. localStorage. getItem ( "new-registered-user" ) === "yes" ; } thisWeek ( row: [ string, Map< string, IParticipant> ] ) : Map< string, IParticipant> { if ( this . isInLessThanSevenDays ( row[ 0 ] ) ) { return row[ 1 ] ; } return new Map ( ) ; } mounted ( ) : void { if ( window. localStorage. getItem ( "welcome-back" ) ) { window. localStorage. removeItem ( "welcome-back" ) ; } if ( window. localStorage. getItem ( "new-registered-user" ) ) { window. localStorage. removeItem ( "new-registered-user" ) ; } } isToday ( date: Date) : boolean { return new Date ( date) . toDateString ( ) === new Date ( ) . toDateString ( ) ; } isTomorrow ( date: string) : boolean { return this . isInDays ( date, 1 ) ; } isInDays ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) === nbDays; } isBefore ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) < nbDays; } isAfter ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) >= nbDays; } isInLessThanSevenDays ( date: string) : boolean { return this . isBefore ( date, 7 ) ; } calculateDiffDays ( date: string) : number { return Math. ceil ( ( new Date ( date) . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 / 60 / 60 / 24 ) ; } get thisWeekGoingToEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isAfter ( event. beginsOn. toDateString ( ) , 0 ) && this . isBefore ( event. beginsOn. toDateString ( ) , 7 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } get goingToEvents ( ) : Map< string, Map< string, IParticipant>> { return this . thisWeekGoingToEvents. reduce ( ( acc: Map< string, Map< string, IParticipant>> , participation: IParticipant ) => { const day = new Date ( participation. event. beginsOn) . toDateString ( ) ; const participations: Map< string, IParticipant> = acc. get ( day) || new Map ( ) ; participations. set ( ` ${ participation. event. uuid} ${ participation. actor. id} ` , participation ) ; acc. set ( day, participations) ; return acc; } , new Map ( ) ) ; } get lastWeekEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isBefore ( event. beginsOn. toDateString ( ) , 0 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } eventDeleted ( eventid: string) : void { this . currentUserParticipations = this . currentUserParticipations. filter ( ( participation ) => participation. event. id !== eventid ) ; } viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; } @Watch ( "loggedUser" ) detectEmptyUserSettings ( loggedUser: IUser) : void { if ( loggedUser && loggedUser. id && loggedUser. settings === null ) { this . $router. push ( { name: RouteName. WELCOME_SCREEN , params: { step: "1" } , } ) ; } } get canShowMyUpcomingEvents ( ) : boolean { return this . currentActor. id != undefined && this . goingToEvents. size > 0 ; } get canShowLastWeekEvents ( ) : boolean { return this . currentActor && this . lastWeekEvents. length > 0 ; } get canShowCloseEvents ( ) : boolean { return this . closeEvents. total > 0 ; } } </ script> < style lang = " scss" scoped > @import "~bulma/sass/utilities/mixins.sass" ; main > div > .container { background : $white; padding : 1rem 0.5rem 3rem; } .search-autocomplete { border : 1px solid #dbdbdb; color : rgba ( 0, 0, 0, 0.87) ; } .events-recent { & > h3 { padding-left : 0.75rem; } .columns { margin : 1rem auto 0; } } .date-component-container { display : flex; align-items : center; margin : 0.5rem auto 1rem; h3.subtitle { margin-left : 7px; } } span.view-all { display : block; margin-top : 1rem; text-align : right; a { text-decoration : underline; } } section.hero { position : relative; z-index : 1; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; opacity : 0.3; z-index : -1; background : url ( "../../public/img/pics/homepage_background-1024w.png" ) ; background-size : cover; } &.webp::before { background-image : url ( "../../public/img/pics/homepage_background-1024w.webp" ) ; } & > .hero-body { padding : 1rem 1.5rem 3rem; } .title { color : $background-color; } .column figure.image img { max-width : 400px; } .instance-description { margin-bottom : 1rem; } } #recent_events { padding : 0; min-height : 20vh; z-index : 10; .title { margin : 20px auto 0; } .columns { margin : 0 auto; } } #picture { .picture-container { position : relative; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; z-index : 1; } & > img { object-fit : cover; max-height : 80vh; display : block; margin : auto; width : 100%; } } .container.section { background : $white; @include tablet { margin-top : -4rem; } z-index : 10; .title { margin : 0 0 10px; font-size : 30px; } .buttons { justify-content : center; margin-top : 2rem; } } } #homepage { background : $white; } .home-separator { background-color : $orange-2; } .clickable { cursor : pointer; } .title { font-size : 27px; &:not(:last-child) { margin-bottom : 0.5rem; } } </ style>
Hyperlink with router 1/5 - $router.push
An hyperlink can be translated as a change of route with the router.
viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; }
In this case, the uuid of the clicked Mobilizon Event is passed to the router to resolve the EVENT
route which is the Event's detail page.
Browse code : js/src/views/Home.vue
Reveal code
< template> < div id = " homepage" > < section class = " hero" :class = " { webp: supportsWebPFormat }" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " hero-body" > < div class = " container" > < h1 class = " title" > {{ config.slogan || $t("Gather â‹… Organize â‹… Mobilize") }} </ h1> < p v-html = " $t(' Join <b>{instance}</b>, a Mobilizon instance' , { instance: config.name, }) " /> < p class = " instance-description" > {{ config.description }}</ p> < b-message type = " is-danger" v-if = " !config.registrationsOpen" > {{ $t("Unfortunately, this instance isn't opened to registrations") }}</ b-message> < div class = " buttons" > < b-button type = " is-primary" tag = " router-link" :to = " { name: RouteName.REGISTER }" v-if = " config.registrationsOpen" > {{ $t("Create an account") }}</ b-button > < b-button type = " is-text" tag = " router-link" :to = " { name: RouteName.ABOUT }" > {{ $t("Learn more about {instance}", { instance: config.name }) }} </ b-button> </ div> </ div> </ div> </ section> < div id = " recent_events" class = " container section" v-if = " config && (!currentUser.id || !currentActor.id)" > < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < b-loading :active.sync = " $apollo.loading" /> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < EventCard :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}</ b-message> </ section> </ div> < div id = " picture" v-if = " config && (!currentUser.id || !currentActor.id)" > < div class = " picture-container" > < picture> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.webp" type = " image/webp" /> < source media = " (max-width: 799px)" srcset = " /img/pics/homepage-480w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.webp" type = " image/webp" /> < source media = " (max-width: 1024px)" srcset = " /img/pics/homepage-1024w.jpg" type = " image/jpeg" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.webp" type = " image/webp" /> < source media = " (max-width: 1920px)" srcset = " /img/pics/homepage-1920w.jpg" type = " image/jpeg" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.webp" type = " image/webp" /> < source media = " (min-width: 1921px)" srcset = " /img/pics/homepage.jpg" type = " image/jpeg" /> < img src = " /img/pics/homepage-1024w.jpg" width = " 3840" height = " 2719" alt = " " loading = " lazy" /> </ picture> </ div> < div class = " container section" > < div class = " columns" > < div class = " column" > < h3 class = " title" > {{ $t("A practical tool") }}</ h3> < p v-html = " $t( ' Mobilizon is a tool that helps you <b>find, create and organise events</b>.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("An ethical alternative") }}</ h3> < p v-html = " $t( ' Ethical alternative to Facebook events, groups and pages, Mobilizon is a <b>tool designed to serve you</b>. Period.' ) " /> </ div> < div class = " column" > < h3 class = " title" > {{ $t("A federated software") }}</ h3> < p v-html = " $t( ' Mobilizon is not a giant platform, but a <b>multitude of interconnected Mobilizon websites</b>.' ) " /> </ div> </ div> < div class = " buttons" > < a class = " button is-primary is-large" href = " https://joinmobilizon.org" > {{ $t("Learn more about Mobilizon") }}</ a > </ div> </ div> </ div> < div class = " container section" v-if = " config && loggedUser && loggedUser.settings" > < section v-if = " currentActor.id && (welcomeBack || newRegisteredUser)" > < b-message type = " is-info" v-if = " welcomeBack" > {{ $t("Welcome back {username}!", { username: currentActor.displayName(), }) }}</ b-message> < b-message type = " is-info" v-if = " newRegisteredUser" > {{ $t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName(), }) }}</ b-message> </ section> < section v-if = " canShowMyUpcomingEvents" > < h2 class = " title" > {{ $t("Your upcoming events") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div v-for = " row of goingToEvents" class = " upcoming-events" :key = " row[0]" > < p class = " date-component-container" v-if = " isInLessThanSevenDays(row[0])" > < span v-if = " isToday(row[0])" > {{ $tc("You have one event today.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isTomorrow(row[0])" > {{ $tc("You have one event tomorrow.", row[1].length, { count: row[1].length, }) }}</ span> < span v-else-if = " isInLessThanSevenDays(row[0])" > {{ $tc("You have one event in {days} days.", row[1].length, { count: row[1].length, days: calculateDiffDays(row[0]), }) }} </ span> </ p> < div> < EventListCard v-for = " participation in thisWeek(row)" @event-deleted = " eventDeleted" :key = " participation[1].id" :participation = " participation[1]" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.MY_EVENTS }" > {{ $t("View everything") }} >></ router-link > </ span> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents && canShowLastWeekEvents" /> < section v-if = " canShowLastWeekEvents" > < h2 class = " title" > {{ $t("Last week") }}</ h2> < b-loading :active.sync = " $apollo.loading" /> < div> < EventListCard v-for = " participation in lastWeekEvents" :key = " participation.id" :participation = " participation" @event-deleted = " eventDeleted" :options = " { hideDate: false }" /> </ div> </ section> < hr class = " home-separator" v-if = " canShowLastWeekEvents && canShowCloseEvents" /> < section class = " events-close" v-if = " canShowCloseEvents" > < h2 class = " title" > {{ $t("Events nearby") }} </ h2> < p> {{ $tc( "Within {number} kilometers of {place}", loggedUser.settings.location.range, { number: loggedUser.settings.location.range, place: loggedUser.settings.location.name, } ) }} < router-link :to = " { name: RouteName.PREFERENCES }" :title = " $t(' Change' )" > < b-icon class = " clickable" icon = " pencil" size = " is-small" /> </ router-link> < b-loading :active.sync = " $apollo.loading" /> </ p> < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in closeEvents.elements.slice(0, 3)" :key = " event.uuid" > < event-card :event = " event" /> </ div> </ div> </ section> < hr class = " home-separator" v-if = " canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents " /> < section class = " events-recent" > < h2 class = " title" > {{ $t("Last published events") }} </ h2> < p> < i18n tag = " span" path = " On {instance} and other federated instances" > < b slot = " instance" > {{ config.name }}</ b> </ i18n> < b-loading :active.sync = " $apollo.loading" /> </ p> < div v-if = " this.events.total > 0" > < div class = " columns is-multiline" > < div class = " column is-one-third-desktop" v-for = " event in this.events.elements.slice(0, 6)" :key = " event.uuid" > < recent-event-card-wrapper :event = " event" /> </ div> </ div> < span class = " view-all" > < router-link :to = " { name: RouteName.SEARCH }" > {{ $t("View everything") }} >></ router-link > </ span> </ div> < b-message v-else type = " is-danger" > {{ $t("No events found") }}< br /> < div v-if = " goingToEvents.size > 0 || lastWeekEvents.length > 0" > < b-icon size = " is-small" icon = " information-outline" /> < small> {{ $t("The events you created are not shown here.") }}</ small> </ div> </ b-message> </ section> </ div> </ div> </ template> < script lang = " ts" > import { Component, Vue, Watch } from "vue-property-decorator" ; import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums" ; import { Paginate } from "@/types/paginate" ; import { supportsWebPFormat } from "@/utils/support" ; import { IParticipant, Participant } from "../types/participant.model" ; import { CLOSE_EVENTS , FETCH_EVENTS } from "../graphql/event" ; import EventListCard from "../components/Event/EventListCard.vue" ; import EventCard from "../components/Event/EventCard.vue" ; import RecentEventCardWrapper from "../components/Event/RecentEventCardWrapper.vue" ; import { CURRENT_ACTOR_CLIENT , LOGGED_USER_PARTICIPATIONS , } from "../graphql/actor" ; import { IPerson, Person } from "../types/actor" ; import { ICurrentUser, IUser } from "../types/current-user.model" ; import { CURRENT_USER_CLIENT , USER_SETTINGS } from "../graphql/user" ; import RouteName from "../router/name" ; import { IEvent } from "../types/event.model" ; import DateComponent from "../components/Event/DateCalendarIcon.vue" ; import { CONFIG } from "../graphql/config" ; import { IConfig } from "../types/config.model" ; import Subtitle from "../components/Utils/Subtitle.vue" ; @Component ( { apollo: { events: { query: FETCH_EVENTS , fetchPolicy: "no-cache" , variables: { orderBy: EventSortField. INSERTED_AT , direction: SortDirection. DESC , } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , update : ( data ) => new Person ( data. currentActor) , } , currentUser: CURRENT_USER_CLIENT , loggedUser: { query: USER_SETTINGS , fetchPolicy: "network-only" , skip ( ) { return ! this . currentUser || this . currentUser. isLoggedIn === false ; } , error ( ) { return null ; } , } , config: CONFIG , currentUserParticipations: { query: LOGGED_USER_PARTICIPATIONS , fetchPolicy: "cache-and-network" , variables ( ) { const lastWeek = new Date ( ) ; lastWeek. setDate ( new Date ( ) . getDate ( ) - 7 ) ; return { afterDateTime: lastWeek. toISOString ( ) , } ; } , update : ( data ) => data. loggedUser. participations. elements. map ( ( participation: IParticipant ) => new Participant ( participation) ) , skip ( ) { return this . currentUser?. isLoggedIn === false ; } , } , closeEvents: { query: CLOSE_EVENTS , variables ( ) { return { location: this . loggedUser?. settings?. location?. geohash, radius: this . loggedUser?. settings?. location?. range, } ; } , update : ( data ) => data. searchEvents, skip ( ) { return ( ! this . currentUser?. isLoggedIn || ! this . loggedUser?. settings?. location?. geohash || ! this . loggedUser?. settings?. location?. range ) ; } , } , } , components: { Subtitle, DateComponent, EventListCard, EventCard, RecentEventCardWrapper, "settings-onboard" : ( ) => import ( "./User/SettingsOnboard.vue" ) , } , metaInfo ( ) { return { title: this . instanceName, titleTemplate: "%s | Mobilizon" , } ; } , } ) export default class Home extends Vue { events: Paginate< IEvent> = { elements: [ ] , total: 0 , } ; locations = [ ] ; city = { name: null } ; country = { name: null } ; currentUser! : IUser; loggedUser! : ICurrentUser; currentActor! : IPerson; config! : IConfig; RouteName = RouteName; currentUserParticipations: IParticipant[ ] = [ ] ; supportsWebPFormat = supportsWebPFormat; closeEvents: Paginate< IEvent> = { elements: [ ] , total: 0 } ; get instanceName ( ) : string | undefined { if ( ! this . config) return undefined ; return this . config. name; } get welcomeBack ( ) : boolean { return window. localStorage. getItem ( "welcome-back" ) === "yes" ; } get newRegisteredUser ( ) : boolean { return window. localStorage. getItem ( "new-registered-user" ) === "yes" ; } thisWeek ( row: [ string, Map< string, IParticipant> ] ) : Map< string, IParticipant> { if ( this . isInLessThanSevenDays ( row[ 0 ] ) ) { return row[ 1 ] ; } return new Map ( ) ; } mounted ( ) : void { if ( window. localStorage. getItem ( "welcome-back" ) ) { window. localStorage. removeItem ( "welcome-back" ) ; } if ( window. localStorage. getItem ( "new-registered-user" ) ) { window. localStorage. removeItem ( "new-registered-user" ) ; } } isToday ( date: Date) : boolean { return new Date ( date) . toDateString ( ) === new Date ( ) . toDateString ( ) ; } isTomorrow ( date: string) : boolean { return this . isInDays ( date, 1 ) ; } isInDays ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) === nbDays; } isBefore ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) < nbDays; } isAfter ( date: string, nbDays: number) : boolean { return this . calculateDiffDays ( date) >= nbDays; } isInLessThanSevenDays ( date: string) : boolean { return this . isBefore ( date, 7 ) ; } calculateDiffDays ( date: string) : number { return Math. ceil ( ( new Date ( date) . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 / 60 / 60 / 24 ) ; } get thisWeekGoingToEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isAfter ( event. beginsOn. toDateString ( ) , 0 ) && this . isBefore ( event. beginsOn. toDateString ( ) , 7 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } get goingToEvents ( ) : Map< string, Map< string, IParticipant>> { return this . thisWeekGoingToEvents. reduce ( ( acc: Map< string, Map< string, IParticipant>> , participation: IParticipant ) => { const day = new Date ( participation. event. beginsOn) . toDateString ( ) ; const participations: Map< string, IParticipant> = acc. get ( day) || new Map ( ) ; participations. set ( ` ${ participation. event. uuid} ${ participation. actor. id} ` , participation ) ; acc. set ( day, participations) ; return acc; } , new Map ( ) ) ; } get lastWeekEvents ( ) : IParticipant[ ] { const res = this . currentUserParticipations. filter ( ( { event, role } ) => event. beginsOn != null && this . isBefore ( event. beginsOn. toDateString ( ) , 0 ) && role !== ParticipantRole. REJECTED ) ; res. sort ( ( a: IParticipant, b: IParticipant ) => a. event. beginsOn. getTime ( ) - b. event. beginsOn. getTime ( ) ) ; return res; } eventDeleted ( eventid: string) : void { this . currentUserParticipations = this . currentUserParticipations. filter ( ( participation ) => participation. event. id !== eventid ) ; } viewEvent ( event: IEvent) : void { this . $router. push ( { name: RouteName. EVENT , params: { uuid: event. uuid } } ) ; } @Watch ( "loggedUser" ) detectEmptyUserSettings ( loggedUser: IUser) : void { if ( loggedUser && loggedUser. id && loggedUser. settings === null ) { this . $router. push ( { name: RouteName. WELCOME_SCREEN , params: { step: "1" } , } ) ; } } get canShowMyUpcomingEvents ( ) : boolean { return this . currentActor. id != undefined && this . goingToEvents. size > 0 ; } get canShowLastWeekEvents ( ) : boolean { return this . currentActor && this . lastWeekEvents. length > 0 ; } get canShowCloseEvents ( ) : boolean { return this . closeEvents. total > 0 ; } } </ script> < style lang = " scss" scoped > @import "~bulma/sass/utilities/mixins.sass" ; main > div > .container { background : $white; padding : 1rem 0.5rem 3rem; } .search-autocomplete { border : 1px solid #dbdbdb; color : rgba ( 0, 0, 0, 0.87) ; } .events-recent { & > h3 { padding-left : 0.75rem; } .columns { margin : 1rem auto 0; } } .date-component-container { display : flex; align-items : center; margin : 0.5rem auto 1rem; h3.subtitle { margin-left : 7px; } } span.view-all { display : block; margin-top : 1rem; text-align : right; a { text-decoration : underline; } } section.hero { position : relative; z-index : 1; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; opacity : 0.3; z-index : -1; background : url ( "../../public/img/pics/homepage_background-1024w.png" ) ; background-size : cover; } &.webp::before { background-image : url ( "../../public/img/pics/homepage_background-1024w.webp" ) ; } & > .hero-body { padding : 1rem 1.5rem 3rem; } .title { color : $background-color; } .column figure.image img { max-width : 400px; } .instance-description { margin-bottom : 1rem; } } #recent_events { padding : 0; min-height : 20vh; z-index : 10; .title { margin : 20px auto 0; } .columns { margin : 0 auto; } } #picture { .picture-container { position : relative; &::before { content : "" ; position : absolute; top : 0; left : 0; width : 100%; height : 100%; z-index : 1; } & > img { object-fit : cover; max-height : 80vh; display : block; margin : auto; width : 100%; } } .container.section { background : $white; @include tablet { margin-top : -4rem; } z-index : 10; .title { margin : 0 0 10px; font-size : 30px; } .buttons { justify-content : center; margin-top : 2rem; } } } #homepage { background : $white; } .home-separator { background-color : $orange-2; } .clickable { cursor : pointer; } .title { font-size : 27px; &:not(:last-child) { margin-bottom : 0.5rem; } } </ style>
Hyperlink with router 2/5 - route definition
The uuid provided previously is passed to render the component views/Event/Event.vue
Browse code : js/src/router/event.ts
Reveal code
import { RouteConfig, Route } from "vue-router" ; import { ImportedComponent } from "vue/types/options" ; const participations = ( ) : Promise < ImportedComponent> => import ( "@/views/Event/Participants.vue" ) ; const editEvent = ( ) : Promise < ImportedComponent> => import ( "@/views/Event/Edit.vue" ) ; const event = ( ) : Promise < ImportedComponent> => import ( "@/views/Event/Event.vue" ) ; const myEvents = ( ) : Promise < ImportedComponent> => import ( "@/views/Event/MyEvents.vue" ) ; export enum EventRouteName { EVENT_LIST = "EventList" , CREATE_EVENT = "CreateEvent" , MY_EVENTS = "MyEvents" , EDIT_EVENT = "EditEvent" , DUPLICATE_EVENT = "DuplicateEvent" , PARTICIPATIONS = "Participations" , EVENT = "Event" , EVENT_PARTICIPATE_WITH_ACCOUNT = "EVENT_PARTICIPATE_WITH_ACCOUNT" , EVENT_PARTICIPATE_WITHOUT_ACCOUNT = "EVENT_PARTICIPATE_WITHOUT_ACCOUNT" , EVENT_PARTICIPATE_LOGGED_OUT = "EVENT_PARTICIPATE_LOGGED_OUT" , EVENT_PARTICIPATE_CONFIRM = "EVENT_PARTICIPATE_CONFIRM" , TAG = "Tag" , } export const eventRoutes: RouteConfig[ ] = [ { path: "/events/list/:location?" , name: EventRouteName. EVENT_LIST , component: ( ) : Promise < ImportedComponent> => import ( "@/views/Event/EventList.vue" ) , meta: { requiredAuth: false } , } , { path: "/events/create" , name: EventRouteName. CREATE_EVENT , component: editEvent, meta: { requiredAuth: true } , } , { path: "/events/me" , name: EventRouteName. MY_EVENTS , component: myEvents, meta: { requiredAuth: true } , } , { path: "/events/edit/:eventId" , name: EventRouteName. EDIT_EVENT , component: editEvent, meta: { requiredAuth: true } , props: ( route: Route) : Record< string , unknown > => { return { ... route. params, ... { isUpdate: true } } ; } , } , { path: "/events/duplicate/:eventId" , name: EventRouteName. DUPLICATE_EVENT , component: editEvent, meta: { requiredAuth: true } , props: ( route: Route) : Record< string , unknown > => ( { ... route. params, ... { isDuplicate: true } , } ) , } , { path: "/events/:eventId/participations" , name: EventRouteName. PARTICIPATIONS , component: participations, meta: { requiredAuth: true } , props: true , } , { path: "/events/:uuid" , name: EventRouteName. EVENT , component: event, props: true , meta: { requiredAuth: false } , } , { path: "/events/:uuid/participate" , name: EventRouteName. EVENT_PARTICIPATE_LOGGED_OUT , component: ( ) : Promise < ImportedComponent> => import ( "../components/Participation/UnloggedParticipation.vue" ) , props: true , } , { path: "/events/:uuid/participate/with-account" , name: EventRouteName. EVENT_PARTICIPATE_WITH_ACCOUNT , component: ( ) : Promise < ImportedComponent> => import ( "../components/Participation/ParticipationWithAccount.vue" ) , props: true , } , { path: "/events/:uuid/participate/without-account" , name: EventRouteName. EVENT_PARTICIPATE_WITHOUT_ACCOUNT , component: ( ) : Promise < ImportedComponent> => import ( "../components/Participation/ParticipationWithoutAccount.vue" ) , props: true , } , { path: "/participation/email/confirm/:token" , name: EventRouteName. EVENT_PARTICIPATE_CONFIRM , component: ( ) : Promise < ImportedComponent> => import ( "../components/Participation/ConfirmParticipation.vue" ) , props: true , } , { path: "/tag/:tag" , name: EventRouteName. TAG , component: ( ) : Promise < ImportedComponent> => import ( "@/views/Search.vue" ) , props: true , meta: { requiredAuth: false } , } , ] ;
Hyperlink with router 3/5 - component prop from router
Here, we can see that the event's uuid from the router is present as a @Prop
for the component.
Browse code : js/src/views/Event/Event.vue
Reveal code
< template> < div class = " container" > < b-loading :active.sync = " $apollo.queries.event.loading" /> < div class = " wrapper" > < event-banner :picture = " event.picture" /> < div class = " intro-wrapper" > < div class = " date-calendar-icon-wrapper" > < date-calendar-icon :date = " event.beginsOn" /> </ div> < section class = " intro" > < div class = " columns" > < div class = " column" > < h1 class = " title" style = " margin : 0" > {{ event.title }}</ h1> < div class = " organizer" > < span v-if = " event.organizerActor && !event.attributedTo" > < popover-actor-card :actor = " event.organizerActor" :inline = " true" > < span> {{ $t("By @{username}", { username: usernameWithDomain(event.organizerActor), }) }} </ span> </ popover-actor-card> </ span> < span v-else-if = " event.attributedTo && event.options.hideOrganizerWhenGroupEvent " > < popover-actor-card :actor = " event.attributedTo" :inline = " true" > {{ $t("By @{group}", { group: usernameWithDomain(event.attributedTo), }) }} </ popover-actor-card> </ span> < span v-else-if = " event.organizerActor && event.attributedTo" > < i18n path = " By {group}" > < popover-actor-card :actor = " event.attributedTo" slot = " group" :inline = " true" > < router-link :to = " { name: RouteName.GROUP, params: { preferredUsername: usernameWithDomain( event.attributedTo ), }, }" > {{ $t("@{group}", { group: usernameWithDomain(event.attributedTo), }) }} </ router-link> </ popover-actor-card> </ i18n> </ span> </ div> < p class = " tags" v-if = " event.tags && event.tags.length > 0" > < router-link v-for = " tag in event.tags" :key = " tag.title" :to = " { name: RouteName.TAG, params: { tag: tag.title } }" > < tag> {{ tag.title }}</ tag> </ router-link> </ p> < b-tag type = " is-warning" size = " is-medium" v-if = " event.draft" > {{ $t("Draft") }} </ b-tag> < span class = " event-status" v-if = " event.status !== EventStatus.CONFIRMED" > < b-tag type = " is-warning" v-if = " event.status === EventStatus.TENTATIVE" > {{ $t("Event to be confirmed") }}</ b-tag > < b-tag type = " is-danger" v-if = " event.status === EventStatus.CANCELLED" > {{ $t("Event cancelled") }}</ b-tag > </ span> </ div> < div class = " column is-3-tablet" > < participation-section :participation = " participations[0]" :event = " event" :anonymousParticipation = " anonymousParticipation" @join-event = " joinEvent" @join-modal = " isJoinModalActive = true" @join-event-with-confirmation = " joinEventWithConfirmation" @confirm-leave = " confirmLeave" @cancel-anonymous-participation = " cancelAnonymousParticipation" /> < div class = " has-text-right" > < template class = " visibility" v-if = " !event.draft" > < p v-if = " event.visibility === EventVisibility.PUBLIC" > {{ $t("Public event") }} < b-icon icon = " earth" /> </ p> < p v-if = " event.visibility === EventVisibility.UNLISTED" > {{ $t("Private event") }} < b-icon icon = " link" /> </ p> </ template> < template v-if = " !event.local && organizer.domain" > < a :href = " event.url" > < tag> {{ organizer.domain }}</ tag> </ a> </ template> < p> < router-link class = " participations-link" v-if = " canManageEvent && event.draft === false" :to = " { name: RouteName.PARTICIPATIONS, params: { eventId: event.uuid }, }" > < span v-if = " event.options.maximumAttendeeCapacity" > {{ $tc( "{available}/{capacity} available places", event.options.maximumAttendeeCapacity - event.participantStats.participant, { available: event.options.maximumAttendeeCapacity - event.participantStats.participant, capacity: event.options.maximumAttendeeCapacity, } ) }} </ span> < span v-else > {{ $tc( "No one is participating|One person participating|{going} people participating", event.participantStats.participant, { going: event.participantStats.participant, } ) }} </ span> </ router-link> < span v-else > < span v-if = " event.options.maximumAttendeeCapacity" > {{ $tc( "{available}/{capacity} available places", event.options.maximumAttendeeCapacity - event.participantStats.participant, { available: event.options.maximumAttendeeCapacity - event.participantStats.participant, capacity: event.options.maximumAttendeeCapacity, } ) }} </ span> < span v-else > {{ $tc( "No one is participating|One person participating|{going} people participating", event.participantStats.participant, { going: event.participantStats.participant, } ) }} </ span> </ span> < b-tooltip type = " is-dark" v-if = " !event.local" :label = " $t( ' The actual number of participants may differ, as this event is hosted on another instance.' ) " > < b-icon size = " is-small" icon = " help-circle-outline" /> </ b-tooltip> < b-icon icon = " ticket-confirmation-outline" /> </ p> < b-dropdown position = " is-bottom-left" aria-role = " list" > < b-button slot = " trigger" role = " button" icon-right = " dots-horizontal" > {{ $t("Actions") }} </ b-button> < b-dropdown-item aria-role = " listitem" has-link v-if = " canManageEvent || event.draft" > < router-link :to = " { name: RouteName.EDIT_EVENT, params: { eventId: event.uuid }, }" > {{ $t("Edit") }} < b-icon icon = " pencil" /> </ router-link> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" has-link v-if = " canManageEvent || event.draft" > < router-link :to = " { name: RouteName.DUPLICATE_EVENT, params: { eventId: event.uuid }, }" > {{ $t("Duplicate") }} < b-icon icon = " content-duplicate" /> </ router-link> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" v-if = " canManageEvent || event.draft" @click = " openDeleteEventModalWrapper" > {{ $t("Delete") }} < b-icon icon = " delete" /> </ b-dropdown-item> < hr class = " dropdown-divider" aria-role = " menuitem" v-if = " canManageEvent || event.draft" /> < b-dropdown-item aria-role = " listitem" v-if = " !event.draft" @click = " triggerShare()" > < span> {{ $t("Share this event") }} < b-icon icon = " share" /> </ span> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" @click = " downloadIcsEvent()" v-if = " !event.draft" > < span> {{ $t("Add to my calendar") }} < b-icon icon = " calendar-plus" /> </ span> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" v-if = " ableToReport" @click = " isReportModalActive = true" > < span> {{ $t("Report") }} < b-icon icon = " flag" /> </ span> </ b-dropdown-item> </ b-dropdown> </ div> </ div> </ div> </ section> </ div> < div class = " event-description-wrapper" > < aside class = " event-metadata" > < div class = " sticky" > < event-metadata-sidebar v-if = " event && config" :event = " event" :config = " config" /> </ div> </ aside> < div class = " event-description-comments" > < section class = " event-description" > < subtitle> {{ $t("About this event") }}</ subtitle> < p v-if = " !event.description" > {{ $t("The event organizer didn't add any description.") }} </ p> < div v-else > < div class = " description-content" ref = " eventDescriptionElement" v-html = " event.description" /> </ div> </ section> < section class = " integration-wrappers" > < component v-for = " (metadata, integration) in integrations" :is = " integration" :key = " integration" :metadata = " metadata" /> </ section> < section class = " comments" ref = " commentsObserver" > < a href = " #comments" > < subtitle id = " comments" > {{ $t("Comments") }}</ subtitle> </ a> < comment-tree v-if = " loadComments" :event = " event" /> </ section> </ div> </ div> < section class = " more-events section" v-if = " event.relatedEvents.length > 0" > < h3 class = " title has-text-centered" > {{ $t("These events may interest you") }} </ h3> < div class = " columns" > < div class = " column is-one-third-desktop" v-for = " relatedEvent in event.relatedEvents" :key = " relatedEvent.uuid" > < EventCard :event = " relatedEvent" /> </ div> </ div> </ section> < b-modal :active.sync = " isReportModalActive" has-modal-card ref = " reportModal" > < report-modal :on-confirm = " reportEvent" :title = " $t(' Report this event' )" :outside-domain = " organizerDomain" @close = " $refs.reportModal.close()" /> </ b-modal> < b-modal :active.sync = " isShareModalActive" has-modal-card ref = " shareModal" > < share-event-modal :event = " event" :eventCapacityOK = " eventCapacityOK" /> </ b-modal> < b-modal :active.sync = " isJoinModalActive" has-modal-card ref = " participationModal" > < identity-picker v-model = " identity" > < template v-slot: footer> < footer class = " modal-card-foot" > < button class = " button" ref = " cancelButton" @click = " isJoinModalActive = false" > {{ $t("Cancel") }} </ button> < button class = " button is-primary" ref = " confirmButton" @click = " event.joinOptions === EventJoinOptions.RESTRICTED ? joinEventWithConfirmation(identity) : joinEvent(identity) " > {{ $t("Confirm my particpation") }} </ button> </ footer> </ template> </ identity-picker> </ b-modal> < b-modal :active.sync = " isJoinConfirmationModalActive" has-modal-card ref = " joinConfirmationModal" > < div class = " modal-card" > < header class = " modal-card-head" > < p class = " modal-card-title" > {{ $t("Participation confirmation") }} </ p> </ header> < section class = " modal-card-body" > < p> {{ $t( "The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?" ) }} </ p> < form @submit.prevent = " joinEvent(actorForConfirmation, messageForConfirmation) " > < b-field :label = " $t(' Message' )" > < b-input type = " textarea" size = " is-medium" v-model = " messageForConfirmation" minlength = " 10" > </ b-input> </ b-field> < div class = " buttons" > < b-button native-type = " button" class = " button" ref = " cancelButton" @click = " isJoinConfirmationModalActive = false" > {{ $t("Cancel") }} </ b-button> < b-button type = " is-primary" native-type = " submit" > {{ $t("Confirm my participation") }} </ b-button> </ div> </ form> </ section> </ div> </ b-modal> </ div> </ div> </ template> < script lang = " ts" > import { Component, Prop, Watch } from "vue-property-decorator" ; import BIcon from "buefy/src/components/icon/Icon.vue" ; import { EventJoinOptions, EventStatus, EventVisibility, MemberRole, ParticipantRole, } from "@/types/enums" ; import { EVENT_PERSON_PARTICIPATION , EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED , FETCH_EVENT , JOIN_EVENT , } from "../../graphql/event" ; import { CURRENT_ACTOR_CLIENT , PERSON_MEMBERSHIP_GROUP , } from "../../graphql/actor" ; import { EventModel, IEvent } from "../../types/event.model" ; import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor" ; import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint" ; import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue" ; import EventCard from "../../components/Event/EventCard.vue" ; import ReportModal from "../../components/Report/ReportModal.vue" ; import { IReport } from "../../types/report.model" ; import { CREATE_REPORT } from "../../graphql/report" ; import EventMixin from "../../mixins/event" ; import IdentityPicker from "../Account/IdentityPicker.vue" ; import ParticipationSection from "../../components/Participation/ParticipationSection.vue" ; import RouteName from "../../router/name" ; import CommentTree from "../../components/Comment/CommentTree.vue" ; import "intersection-observer" ; import { CONFIG } from "../../graphql/config" ; import { AnonymousParticipationNotFoundError, getLeaveTokenForParticipation, isParticipatingInThisEvent, removeAnonymousParticipation, } from "../../services/AnonymousParticipationStorage" ; import { IConfig } from "../../types/config.model" ; import Subtitle from "../../components/Utils/Subtitle.vue" ; import Tag from "../../components/Tag.vue" ; import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue" ; import EventBanner from "../../components/Event/EventBanner.vue" ; import PopoverActorCard from "../../components/Account/PopoverActorCard.vue" ; import { IParticipant } from "../../types/participant.model" ; import { ApolloCache, FetchResult } from "@apollo/client/core" ; import { IEventMetadataDescription } from "@/types/event-metadata" ; import { eventMetaDataList } from "../../services/EventMetadata" ; @Component ( { components: { Subtitle, EventCard, BIcon, DateCalendarIcon, ReportModal, IdentityPicker, ParticipationSection, CommentTree, Tag, PopoverActorCard, EventBanner, EventMetadataSidebar, ShareEventModal : ( ) => import ( "../../components/Event/ShareEventModal.vue" ) , "integration-twitch" : ( ) => import ( "../../components/Event/Integrations/Twitch.vue" ) , "integration-peertube" : ( ) => import ( "../../components/Event/Integrations/PeerTube.vue" ) , "integration-youtube" : ( ) => import ( "../../components/Event/Integrations/YouTube.vue" ) , "integration-jitsi-meet" : ( ) => import ( "../../components/Event/Integrations/JitsiMeet.vue" ) , "integration-etherpad" : ( ) => import ( "../../components/Event/Integrations/Etherpad.vue" ) , } , apollo: { event: { query: FETCH_EVENT , fetchPolicy: "cache-and-network" , variables ( ) { return { uuid: this . uuid, } ; } , error ( { graphQLErrors } ) { this . handleErrors ( graphQLErrors) ; } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , } , participations: { query: EVENT_PERSON_PARTICIPATION , fetchPolicy: "cache-and-network" , variables ( ) { return { eventId: this . event. id, actorId: this . currentActor. id, } ; } , subscribeToMore: { document: EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED , variables ( ) { return { eventId: this . event. id, actorId: this . currentActor. id, } ; } , } , update : ( data ) => { if ( data && data. person) return data. person. participations. elements; return [ ] ; } , skip ( ) { return ( ! this . currentActor || ! this . event || ! this . event. id || ! this . currentActor. id ) ; } , } , person: { query: PERSON_MEMBERSHIP_GROUP , fetchPolicy: "cache-and-network" , variables ( ) { return { id: this . currentActor. id, group: usernameWithDomain ( this . event?. attributedTo) , } ; } , skip ( ) { return ( ! this . currentActor. id || ! this . event?. attributedTo || ! this . event?. attributedTo?. preferredUsername ) ; } , } , config: CONFIG , } , metaInfo ( ) { return { title: this . eventTitle, meta: [ { name: "description" , content: this . eventDescription } , ] , } ; } , } ) export default class Event extends EventMixin { @Prop ( { type: String, required: true } ) uuid! : string; event: IEvent = new EventModel ( ) ; currentActor! : IPerson; identity: IPerson = new Person ( ) ; config! : IConfig; person! : IPerson; participations: IParticipant[ ] = [ ] ; oldParticipationRole! : string; isReportModalActive = false ; isShareModalActive = false ; isJoinModalActive = false ; isJoinConfirmationModalActive = false ; EventVisibility = EventVisibility; EventStatus = EventStatus; EventJoinOptions = EventJoinOptions; usernameWithDomain = usernameWithDomain; RouteName = RouteName; observer! : IntersectionObserver; loadComments = false ; anonymousParticipation: boolean | null = null ; actorForConfirmation! : IPerson; messageForConfirmation = "" ; get eventTitle ( ) : undefined | string { if ( ! this . event) return undefined ; return this . event. title; } get eventDescription ( ) : undefined | string { if ( ! this . event) return undefined ; return this . event. description; } async mounted ( ) : Promise< void > { this . identity = this . currentActor; if ( this . $route. hash. includes ( "#comment-" ) ) { this . loadComments = true ; } try { if ( window. isSecureContext) { this . anonymousParticipation = await this . anonymousParticipationConfirmed ( ) ; } } catch ( e) { if ( e instanceof AnonymousParticipationNotFoundError ) { this . anonymousParticipation = null ; } else { console. error ( e) ; } } this . observer = new IntersectionObserver ( ( entries ) => { for ( const entry of entries) { if ( entry) { this . loadComments = entry. isIntersecting || this . loadComments; } } } , { rootMargin: "-50px 0px -50px" , } ) ; this . observer. observe ( this . $refs. commentsObserver as Element) ; this . $watch ( "eventDescription" , ( eventDescription ) => { if ( ! eventDescription) return ; const eventDescriptionElement = this . $refs . eventDescriptionElement as HTMLElement; eventDescriptionElement. addEventListener ( "click" , ( $event ) => { let { target } : { target: any } = $event; while ( target && target. tagName !== "A" ) target = target. parentNode; if ( target && target. matches ( ".hashtag" ) && target. href) { const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented, } = $event; if ( metaKey || altKey || ctrlKey || shiftKey) return ; if ( defaultPrevented) return ; if ( button !== undefined && button !== 0 ) return ; if ( target && target. getAttribute) { const linkTarget = target. getAttribute ( "target" ) ; if ( / \b_blank\b / i . test ( linkTarget) ) return ; } const url = new URL ( target. href) ; const to = url. pathname; if ( window. location. pathname !== to && $event. preventDefault) { $event. preventDefault ( ) ; this . $router. push ( to) ; } } } ) ; } ) ; this . $on ( "event-deleted" , ( ) => { return this . $router. push ( { name: RouteName. HOME } ) ; } ) ; } async openDeleteEventModalWrapper ( ) : Promise< void > { await this . openDeleteEventModal ( this . event) ; } async reportEvent ( content: string, forward: boolean) : Promise< void > { this . isReportModalActive = false ; this . $refs. reportModal. close ( ) ; if ( ! this . organizer) return ; const eventTitle = this . event. title; try { await this . $apollo. mutate< IReport> ( { mutation: CREATE_REPORT , variables: { eventId: this . event. id, reportedId: this . organizer ? this . organizer. id : null , content, forward, } , } ) ; this . $notifier. success ( this . $t ( "Event {eventTitle} reported" , { eventTitle } ) as string ) ; } catch ( error) { console. error ( error) ; } } joinEventWithConfirmation ( actor: IPerson) : void { this . isJoinConfirmationModalActive = true ; this . actorForConfirmation = actor; } async joinEvent ( identity: IPerson, message: string | null = null ) : Promise< void > { this . isJoinConfirmationModalActive = false ; this . isJoinModalActive = false ; try { const { data: mutationData } = await this . $apollo. mutate< { joinEvent: IParticipant; } > ( { mutation: JOIN_EVENT , variables: { eventId: this . event. id, actorId: identity. id, message, } , update : ( store: ApolloCache< { joinEvent: IParticipant; } > , { data } : FetchResult ) => { if ( data == null ) return ; const participationCachedData = store. readQuery< { person: IPerson } > ( { query: EVENT_PERSON_PARTICIPATION , variables: { eventId: this . event. id, actorId: identity. id } , } ) ; if ( participationCachedData?. person == undefined ) { console. error ( "Cannot update participation cache, because of null value." ) ; return ; } store. writeQuery ( { query: EVENT_PERSON_PARTICIPATION , variables: { eventId: this . event. id, actorId: identity. id } , data: { person: { ... participationCachedData?. person, participations: { elements: [ data. joinEvent] , total: 1 , } , } , } , } ) ; const cachedData = store. readQuery< { event: IEvent } > ( { query: FETCH_EVENT , variables: { uuid: this . event. uuid } , } ) ; if ( cachedData == null ) return ; const { event } = cachedData; if ( event === null ) { console. error ( "Cannot update event participant cache, because of null value." ) ; return ; } const participantStats = { ... event. participantStats } ; if ( data. joinEvent. role === ParticipantRole. NOT_APPROVED ) { participantStats. notApproved += 1 ; } else { participantStats. going += 1 ; participantStats. participant += 1 ; } store. writeQuery ( { query: FETCH_EVENT , variables: { uuid: this . uuid } , data: { event: { ... event, participantStats, } , } , } ) ; } , } ) ; if ( mutationData) { if ( mutationData. joinEvent. role === ParticipantRole. NOT_APPROVED ) { this . participationRequestedMessage ( ) ; } else { this . participationConfirmedMessage ( ) ; } } } catch ( error) { console. error ( error) ; } } confirmLeave ( ) : void { this . $buefy. dialog. confirm ( { title: this . $t ( 'Leaving event "{title}"' , { title: this . event. title, } ) as string, message: this . $t ( 'Are you sure you want to cancel your participation at event "{title}"?' , { title: this . event. title, } ) as string, confirmText: this . $t ( "Leave event" ) as string, cancelText: this . $t ( "Cancel" ) as string, type: "is-danger" , hasIcon: true , onConfirm : ( ) => { if ( this . currentActor. id) { this . leaveEvent ( this . event, this . currentActor. id) ; } } , } ) ; } @Watch ( "participations" ) watchParticipations ( ) : void { if ( this . participations. length > 0 ) { if ( this . oldParticipationRole && this . participations[ 0 ] . role !== ParticipantRole. NOT_APPROVED && this . oldParticipationRole !== this . participations[ 0 ] . role ) { switch ( this . participations[ 0 ] . role) { case ParticipantRole. PARTICIPANT : this . participationConfirmedMessage ( ) ; break ; case ParticipantRole. REJECTED : this . participationRejectedMessage ( ) ; break ; default : this . participationChangedMessage ( ) ; break ; } } this . oldParticipationRole = this . participations[ 0 ] . role; } } private participationConfirmedMessage ( ) { this . $notifier. success ( this . $t ( "Your participation has been confirmed" ) as string ) ; } private participationRequestedMessage ( ) { this . $notifier. success ( this . $t ( "Your participation has been requested" ) as string ) ; } private participationRejectedMessage ( ) { this . $notifier. error ( this . $t ( "Your participation has been rejected" ) as string ) ; } private participationChangedMessage ( ) { this . $notifier. info ( this . $t ( "Your participation status has been changed" ) as string ) ; } async downloadIcsEvent ( ) : Promise< void > { const data = await ( await fetch ( ` ${ GRAPHQL_API_ENDPOINT } /events/ ${ this . uuid} /export/ics ` ) ) . text ( ) ; const blob = new Blob ( [ data] , { type: "text/calendar" } ) ; const link = document. createElement ( "a" ) ; link. href = window. URL . createObjectURL ( blob) ; link. download = ` ${ this . event. title} .ics ` ; document. body. appendChild ( link) ; link. click ( ) ; document. body. removeChild ( link) ; } triggerShare ( ) : void { if ( navigator. share) { navigator . share ( { title: this . event. title, url: this . event. url, } ) . then ( ( ) => console. log ( "Successful share" ) ) . catch ( ( error: any ) => console. log ( "Error sharing" , error) ) ; } else { this . isShareModalActive = true ; } } handleErrors ( errors: any[ ] ) : void { if ( errors. some ( ( error ) => error. status_code === 404 ) || errors. some ( ( { message } ) => message. includes ( "has invalid value $uuid" ) ) ) { this . $router. replace ( { name: RouteName. PAGE_NOT_FOUND } ) ; } } get actorIsParticipant ( ) : boolean { if ( this . actorIsOrganizer) return true ; return ( this . participations. length > 0 && this . participations[ 0 ] . role === ParticipantRole. PARTICIPANT ) ; } get actorIsOrganizer ( ) : boolean { return ( this . participations. length > 0 && this . participations[ 0 ] . role === ParticipantRole. CREATOR ) ; } get hasGroupPrivileges ( ) : boolean { return ( this . person?. memberships?. total > 0 && [ MemberRole. MODERATOR , MemberRole. ADMINISTRATOR ] . includes ( this . person?. memberships?. elements[ 0 ] . role ) ) ; } get canManageEvent ( ) : boolean { return this . actorIsOrganizer || this . hasGroupPrivileges; } get endDate ( ) : Date { return this . event. endsOn !== null && this . event. endsOn > this . event. beginsOn ? this . event. endsOn : this . event. beginsOn; } get eventCapacityOK ( ) : boolean { if ( this . event. draft) return true ; if ( ! this . event. options. maximumAttendeeCapacity) return true ; return ( this . event. options. maximumAttendeeCapacity > this . event. participantStats. participant ) ; } get numberOfPlacesStillAvailable ( ) : number { if ( this . event. draft) return this . event. options. maximumAttendeeCapacity; return ( this . event. options. maximumAttendeeCapacity - this . event. participantStats. participant ) ; } async anonymousParticipationConfirmed ( ) : Promise< boolean> { return isParticipatingInThisEvent ( this . uuid) ; } async cancelAnonymousParticipation ( ) : Promise< void > { const token = ( await getLeaveTokenForParticipation ( this . uuid) ) as string; await this . leaveEvent ( this . event, this . config. anonymous. actorId, token) ; await removeAnonymousParticipation ( this . uuid) ; this . anonymousParticipation = null ; } get ableToReport ( ) : boolean { return ( this . config && ( this . currentActor. id != null || this . config. anonymous. reports. allowed) ) ; } get organizer ( ) : IActor | null { if ( this . event. attributedTo && this . event. attributedTo. id) { return this . event. attributedTo; } if ( this . event. organizerActor) { return this . event. organizerActor; } return null ; } get organizerDomain ( ) : string | null { if ( this . organizer) { return this . organizer. domain; } return null ; } metadataToComponent: Record< string, string> = { "mz:live:twitch:url" : "integration-twitch" , "mz:live:peertube:url" : "integration-peertube" , "mz:live:youtube:url" : "integration-youtube" , "mz:visio:jitsi_meet" : "integration-jitsi-meet" , "mz:notes:etherpad:url" : "integration-etherpad" , } ; get integrations ( ) : Record< string, IEventMetadataDescription> { return this . event. metadata . map ( ( val ) => { const def = eventMetaDataList. find ( ( dat ) => dat. key === val. key) ; return { ... def, ... val, } ; } ) . reduce ( ( acc: Record< string, IEventMetadataDescription> , metadata ) => { const component = this . metadataToComponent[ metadata. key] ; if ( component !== undefined ) { acc[ component] = metadata; } return acc; } , { } ) ; } } </ script> < style lang = " scss" scoped > .section { padding : 1rem 2rem 4rem; } .fade-enter-active, .fade-leave-active { transition : opacity 0.5s; } .fade-enter, .fade-leave-to { opacity : 0; } div.sidebar { display : flex; flex-wrap : wrap; flex-direction : column; position : relative; &::before { content : "" ; background : #b3b3b2; position : absolute; bottom : 30px; top : 30px; left : 0; height : calc ( 100% - 60px) ; width : 1px; } div.organizer { display : inline-flex; padding-top : 10px; a { color : #4a4a4a; span { line-height : 2.7rem; padding-right : 6px; } } } } .intro { background : white; .is-3-tablet { width : initial; } p.tags { a { text-decoration : none; } span { &.tag { margin : 0 2px; } } } } .event-description-wrapper { display : flex; flex-wrap : wrap; flex-direction : column; padding : 0; @media all and ( min-width : 672px) { flex-direction : row-reverse; } & > aside, & > div { @media all and ( min-width : 672px) { margin : 2rem auto; } } aside.event-metadata { min-width : 20rem; flex : 1; @media all and ( min-width : 672px) { padding-left : 1rem; } .sticky { position : sticky; background : white; top : 50px; padding : 1rem; } } div.event-description-comments { min-width : 20rem; padding : 1rem; flex : 2; background : white; } .description-content { ::v-deep h1 { font-size : 2rem; } ::v-deep h2 { font-size : 1.5rem; } ::v-deep h3 { font-size : 1.25rem; } ::v-deep ul { list-style-type : disc; } ::v-deep li { margin : 10px auto 10px 2rem; } ::v-deep blockquote { border-left : 0.2em solid #333; display : block; padding-left : 1em; } ::v-deep p { margin : 10px auto; a { display : inline-block; padding : 0.3rem; background : $secondary; color : #111; &:empty { display : none; } } } } } .comments { padding-top : 3rem; a h3#comments { margin-bottom : 10px; } } .more-events { background : white; padding : 1rem 1rem 4rem; & > .title { font-size : 1.5rem; } } .dropdown .dropdown-trigger span { cursor : pointer; } a.dropdown-item,.dropdown .dropdown-menu .has-link a, button.dropdown-item { white-space : nowrap; width : 100%; padding-right : 1rem; text-align : right; } a.participations-link { text-decoration : none; } .event-status .tag { font-size : 1rem; } .no-border { border : 0; cursor : auto; } .wrapper, .intro-wrapper { display : flex; flex-direction : column; } .intro-wrapper { position : relative; padding : 0 16px 16px; background : #fff; .date-calendar-icon-wrapper { margin-top : 16px; height : 0; display : flex; align-items : flex-end; align-self : flex-start; margin-bottom : 7px; margin-left : 0rem; } } .title { margin : 0; font-size : 2rem; } </ style>
Hyperlink with router 4/5 - variable in GraphQL
The GraphQL query can then rely on the prop uuid
from the component.
This way GraphQL can use it as $uuid
named variable.
Browse code : js/src/views/Event/Event.vue
Reveal code
< template> < div class = " container" > < b-loading :active.sync = " $apollo.queries.event.loading" /> < div class = " wrapper" > < event-banner :picture = " event.picture" /> < div class = " intro-wrapper" > < div class = " date-calendar-icon-wrapper" > < date-calendar-icon :date = " event.beginsOn" /> </ div> < section class = " intro" > < div class = " columns" > < div class = " column" > < h1 class = " title" style = " margin : 0" > {{ event.title }}</ h1> < div class = " organizer" > < span v-if = " event.organizerActor && !event.attributedTo" > < popover-actor-card :actor = " event.organizerActor" :inline = " true" > < span> {{ $t("By @{username}", { username: usernameWithDomain(event.organizerActor), }) }} </ span> </ popover-actor-card> </ span> < span v-else-if = " event.attributedTo && event.options.hideOrganizerWhenGroupEvent " > < popover-actor-card :actor = " event.attributedTo" :inline = " true" > {{ $t("By @{group}", { group: usernameWithDomain(event.attributedTo), }) }} </ popover-actor-card> </ span> < span v-else-if = " event.organizerActor && event.attributedTo" > < i18n path = " By {group}" > < popover-actor-card :actor = " event.attributedTo" slot = " group" :inline = " true" > < router-link :to = " { name: RouteName.GROUP, params: { preferredUsername: usernameWithDomain( event.attributedTo ), }, }" > {{ $t("@{group}", { group: usernameWithDomain(event.attributedTo), }) }} </ router-link> </ popover-actor-card> </ i18n> </ span> </ div> < p class = " tags" v-if = " event.tags && event.tags.length > 0" > < router-link v-for = " tag in event.tags" :key = " tag.title" :to = " { name: RouteName.TAG, params: { tag: tag.title } }" > < tag> {{ tag.title }}</ tag> </ router-link> </ p> < b-tag type = " is-warning" size = " is-medium" v-if = " event.draft" > {{ $t("Draft") }} </ b-tag> < span class = " event-status" v-if = " event.status !== EventStatus.CONFIRMED" > < b-tag type = " is-warning" v-if = " event.status === EventStatus.TENTATIVE" > {{ $t("Event to be confirmed") }}</ b-tag > < b-tag type = " is-danger" v-if = " event.status === EventStatus.CANCELLED" > {{ $t("Event cancelled") }}</ b-tag > </ span> </ div> < div class = " column is-3-tablet" > < participation-section :participation = " participations[0]" :event = " event" :anonymousParticipation = " anonymousParticipation" @join-event = " joinEvent" @join-modal = " isJoinModalActive = true" @join-event-with-confirmation = " joinEventWithConfirmation" @confirm-leave = " confirmLeave" @cancel-anonymous-participation = " cancelAnonymousParticipation" /> < div class = " has-text-right" > < template class = " visibility" v-if = " !event.draft" > < p v-if = " event.visibility === EventVisibility.PUBLIC" > {{ $t("Public event") }} < b-icon icon = " earth" /> </ p> < p v-if = " event.visibility === EventVisibility.UNLISTED" > {{ $t("Private event") }} < b-icon icon = " link" /> </ p> </ template> < template v-if = " !event.local && organizer.domain" > < a :href = " event.url" > < tag> {{ organizer.domain }}</ tag> </ a> </ template> < p> < router-link class = " participations-link" v-if = " canManageEvent && event.draft === false" :to = " { name: RouteName.PARTICIPATIONS, params: { eventId: event.uuid }, }" > < span v-if = " event.options.maximumAttendeeCapacity" > {{ $tc( "{available}/{capacity} available places", event.options.maximumAttendeeCapacity - event.participantStats.participant, { available: event.options.maximumAttendeeCapacity - event.participantStats.participant, capacity: event.options.maximumAttendeeCapacity, } ) }} </ span> < span v-else > {{ $tc( "No one is participating|One person participating|{going} people participating", event.participantStats.participant, { going: event.participantStats.participant, } ) }} </ span> </ router-link> < span v-else > < span v-if = " event.options.maximumAttendeeCapacity" > {{ $tc( "{available}/{capacity} available places", event.options.maximumAttendeeCapacity - event.participantStats.participant, { available: event.options.maximumAttendeeCapacity - event.participantStats.participant, capacity: event.options.maximumAttendeeCapacity, } ) }} </ span> < span v-else > {{ $tc( "No one is participating|One person participating|{going} people participating", event.participantStats.participant, { going: event.participantStats.participant, } ) }} </ span> </ span> < b-tooltip type = " is-dark" v-if = " !event.local" :label = " $t( ' The actual number of participants may differ, as this event is hosted on another instance.' ) " > < b-icon size = " is-small" icon = " help-circle-outline" /> </ b-tooltip> < b-icon icon = " ticket-confirmation-outline" /> </ p> < b-dropdown position = " is-bottom-left" aria-role = " list" > < b-button slot = " trigger" role = " button" icon-right = " dots-horizontal" > {{ $t("Actions") }} </ b-button> < b-dropdown-item aria-role = " listitem" has-link v-if = " canManageEvent || event.draft" > < router-link :to = " { name: RouteName.EDIT_EVENT, params: { eventId: event.uuid }, }" > {{ $t("Edit") }} < b-icon icon = " pencil" /> </ router-link> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" has-link v-if = " canManageEvent || event.draft" > < router-link :to = " { name: RouteName.DUPLICATE_EVENT, params: { eventId: event.uuid }, }" > {{ $t("Duplicate") }} < b-icon icon = " content-duplicate" /> </ router-link> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" v-if = " canManageEvent || event.draft" @click = " openDeleteEventModalWrapper" > {{ $t("Delete") }} < b-icon icon = " delete" /> </ b-dropdown-item> < hr class = " dropdown-divider" aria-role = " menuitem" v-if = " canManageEvent || event.draft" /> < b-dropdown-item aria-role = " listitem" v-if = " !event.draft" @click = " triggerShare()" > < span> {{ $t("Share this event") }} < b-icon icon = " share" /> </ span> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" @click = " downloadIcsEvent()" v-if = " !event.draft" > < span> {{ $t("Add to my calendar") }} < b-icon icon = " calendar-plus" /> </ span> </ b-dropdown-item> < b-dropdown-item aria-role = " listitem" v-if = " ableToReport" @click = " isReportModalActive = true" > < span> {{ $t("Report") }} < b-icon icon = " flag" /> </ span> </ b-dropdown-item> </ b-dropdown> </ div> </ div> </ div> </ section> </ div> < div class = " event-description-wrapper" > < aside class = " event-metadata" > < div class = " sticky" > < event-metadata-sidebar v-if = " event && config" :event = " event" :config = " config" /> </ div> </ aside> < div class = " event-description-comments" > < section class = " event-description" > < subtitle> {{ $t("About this event") }}</ subtitle> < p v-if = " !event.description" > {{ $t("The event organizer didn't add any description.") }} </ p> < div v-else > < div class = " description-content" ref = " eventDescriptionElement" v-html = " event.description" /> </ div> </ section> < section class = " integration-wrappers" > < component v-for = " (metadata, integration) in integrations" :is = " integration" :key = " integration" :metadata = " metadata" /> </ section> < section class = " comments" ref = " commentsObserver" > < a href = " #comments" > < subtitle id = " comments" > {{ $t("Comments") }}</ subtitle> </ a> < comment-tree v-if = " loadComments" :event = " event" /> </ section> </ div> </ div> < section class = " more-events section" v-if = " event.relatedEvents.length > 0" > < h3 class = " title has-text-centered" > {{ $t("These events may interest you") }} </ h3> < div class = " columns" > < div class = " column is-one-third-desktop" v-for = " relatedEvent in event.relatedEvents" :key = " relatedEvent.uuid" > < EventCard :event = " relatedEvent" /> </ div> </ div> </ section> < b-modal :active.sync = " isReportModalActive" has-modal-card ref = " reportModal" > < report-modal :on-confirm = " reportEvent" :title = " $t(' Report this event' )" :outside-domain = " organizerDomain" @close = " $refs.reportModal.close()" /> </ b-modal> < b-modal :active.sync = " isShareModalActive" has-modal-card ref = " shareModal" > < share-event-modal :event = " event" :eventCapacityOK = " eventCapacityOK" /> </ b-modal> < b-modal :active.sync = " isJoinModalActive" has-modal-card ref = " participationModal" > < identity-picker v-model = " identity" > < template v-slot: footer> < footer class = " modal-card-foot" > < button class = " button" ref = " cancelButton" @click = " isJoinModalActive = false" > {{ $t("Cancel") }} </ button> < button class = " button is-primary" ref = " confirmButton" @click = " event.joinOptions === EventJoinOptions.RESTRICTED ? joinEventWithConfirmation(identity) : joinEvent(identity) " > {{ $t("Confirm my particpation") }} </ button> </ footer> </ template> </ identity-picker> </ b-modal> < b-modal :active.sync = " isJoinConfirmationModalActive" has-modal-card ref = " joinConfirmationModal" > < div class = " modal-card" > < header class = " modal-card-head" > < p class = " modal-card-title" > {{ $t("Participation confirmation") }} </ p> </ header> < section class = " modal-card-body" > < p> {{ $t( "The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?" ) }} </ p> < form @submit.prevent = " joinEvent(actorForConfirmation, messageForConfirmation) " > < b-field :label = " $t(' Message' )" > < b-input type = " textarea" size = " is-medium" v-model = " messageForConfirmation" minlength = " 10" > </ b-input> </ b-field> < div class = " buttons" > < b-button native-type = " button" class = " button" ref = " cancelButton" @click = " isJoinConfirmationModalActive = false" > {{ $t("Cancel") }} </ b-button> < b-button type = " is-primary" native-type = " submit" > {{ $t("Confirm my participation") }} </ b-button> </ div> </ form> </ section> </ div> </ b-modal> </ div> </ div> </ template> < script lang = " ts" > import { Component, Prop, Watch } from "vue-property-decorator" ; import BIcon from "buefy/src/components/icon/Icon.vue" ; import { EventJoinOptions, EventStatus, EventVisibility, MemberRole, ParticipantRole, } from "@/types/enums" ; import { EVENT_PERSON_PARTICIPATION , EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED , FETCH_EVENT , JOIN_EVENT , } from "../../graphql/event" ; import { CURRENT_ACTOR_CLIENT , PERSON_MEMBERSHIP_GROUP , } from "../../graphql/actor" ; import { EventModel, IEvent } from "../../types/event.model" ; import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor" ; import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint" ; import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue" ; import EventCard from "../../components/Event/EventCard.vue" ; import ReportModal from "../../components/Report/ReportModal.vue" ; import { IReport } from "../../types/report.model" ; import { CREATE_REPORT } from "../../graphql/report" ; import EventMixin from "../../mixins/event" ; import IdentityPicker from "../Account/IdentityPicker.vue" ; import ParticipationSection from "../../components/Participation/ParticipationSection.vue" ; import RouteName from "../../router/name" ; import CommentTree from "../../components/Comment/CommentTree.vue" ; import "intersection-observer" ; import { CONFIG } from "../../graphql/config" ; import { AnonymousParticipationNotFoundError, getLeaveTokenForParticipation, isParticipatingInThisEvent, removeAnonymousParticipation, } from "../../services/AnonymousParticipationStorage" ; import { IConfig } from "../../types/config.model" ; import Subtitle from "../../components/Utils/Subtitle.vue" ; import Tag from "../../components/Tag.vue" ; import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue" ; import EventBanner from "../../components/Event/EventBanner.vue" ; import PopoverActorCard from "../../components/Account/PopoverActorCard.vue" ; import { IParticipant } from "../../types/participant.model" ; import { ApolloCache, FetchResult } from "@apollo/client/core" ; import { IEventMetadataDescription } from "@/types/event-metadata" ; import { eventMetaDataList } from "../../services/EventMetadata" ; @Component ( { components: { Subtitle, EventCard, BIcon, DateCalendarIcon, ReportModal, IdentityPicker, ParticipationSection, CommentTree, Tag, PopoverActorCard, EventBanner, EventMetadataSidebar, ShareEventModal : ( ) => import ( "../../components/Event/ShareEventModal.vue" ) , "integration-twitch" : ( ) => import ( "../../components/Event/Integrations/Twitch.vue" ) , "integration-peertube" : ( ) => import ( "../../components/Event/Integrations/PeerTube.vue" ) , "integration-youtube" : ( ) => import ( "../../components/Event/Integrations/YouTube.vue" ) , "integration-jitsi-meet" : ( ) => import ( "../../components/Event/Integrations/JitsiMeet.vue" ) , "integration-etherpad" : ( ) => import ( "../../components/Event/Integrations/Etherpad.vue" ) , } , apollo: { event: { query: FETCH_EVENT , fetchPolicy: "cache-and-network" , variables ( ) { return { uuid: this . uuid, } ; } , error ( { graphQLErrors } ) { this . handleErrors ( graphQLErrors) ; } , } , currentActor: { query: CURRENT_ACTOR_CLIENT , } , participations: { query: EVENT_PERSON_PARTICIPATION , fetchPolicy: "cache-and-network" , variables ( ) { return { eventId: this . event. id, actorId: this . currentActor. id, } ; } , subscribeToMore: { document: EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED , variables ( ) { return { eventId: this . event. id, actorId: this . currentActor. id, } ; } , } , update : ( data ) => { if ( data && data. person) return data. person. participations. elements; return [ ] ; } , skip ( ) { return ( ! this . currentActor || ! this . event || ! this . event. id || ! this . currentActor. id ) ; } , } , person: { query: PERSON_MEMBERSHIP_GROUP , fetchPolicy: "cache-and-network" , variables ( ) { return { id: this . currentActor. id, group: usernameWithDomain ( this . event?. attributedTo) , } ; } , skip ( ) { return ( ! this . currentActor. id || ! this . event?. attributedTo || ! this . event?. attributedTo?. preferredUsername ) ; } , } , config: CONFIG , } , metaInfo ( ) { return { title: this . eventTitle, meta: [ { name: "description" , content: this . eventDescription } , ] , } ; } , } ) export default class Event extends EventMixin { @Prop ( { type: String, required: true } ) uuid! : string; event: IEvent = new EventModel ( ) ; currentActor! : IPerson; identity: IPerson = new Person ( ) ; config! : IConfig; person! : IPerson; participations: IParticipant[ ] = [ ] ; oldParticipationRole! : string; isReportModalActive = false ; isShareModalActive = false ; isJoinModalActive = false ; isJoinConfirmationModalActive = false ; EventVisibility = EventVisibility; EventStatus = EventStatus; EventJoinOptions = EventJoinOptions; usernameWithDomain = usernameWithDomain; RouteName = RouteName; observer! : IntersectionObserver; loadComments = false ; anonymousParticipation: boolean | null = null ; actorForConfirmation! : IPerson; messageForConfirmation = "" ; get eventTitle ( ) : undefined | string { if ( ! this . event) return undefined ; return this . event. title; } get eventDescription ( ) : undefined | string { if ( ! this . event) return undefined ; return this . event. description; } async mounted ( ) : Promise< void > { this . identity = this . currentActor; if ( this . $route. hash. includes ( "#comment-" ) ) { this . loadComments = true ; } try { if ( window. isSecureContext) { this . anonymousParticipation = await this . anonymousParticipationConfirmed ( ) ; } } catch ( e) { if ( e instanceof AnonymousParticipationNotFoundError ) { this . anonymousParticipation = null ; } else { console. error ( e) ; } } this . observer = new IntersectionObserver ( ( entries ) => { for ( const entry of entries) { if ( entry) { this . loadComments = entry. isIntersecting || this . loadComments; } } } , { rootMargin: "-50px 0px -50px" , } ) ; this . observer. observe ( this . $refs. commentsObserver as Element) ; this . $watch ( "eventDescription" , ( eventDescription ) => { if ( ! eventDescription) return ; const eventDescriptionElement = this . $refs . eventDescriptionElement as HTMLElement; eventDescriptionElement. addEventListener ( "click" , ( $event ) => { let { target } : { target: any } = $event; while ( target && target. tagName !== "A" ) target = target. parentNode; if ( target && target. matches ( ".hashtag" ) && target. href) { const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented, } = $event; if ( metaKey || altKey || ctrlKey || shiftKey) return ; if ( defaultPrevented) return ; if ( button !== undefined && button !== 0 ) return ; if ( target && target. getAttribute) { const linkTarget = target. getAttribute ( "target" ) ; if ( / \b_blank\b / i . test ( linkTarget) ) return ; } const url = new URL ( target. href) ; const to = url. pathname; if ( window. location. pathname !== to && $event. preventDefault) { $event. preventDefault ( ) ; this . $router. push ( to) ; } } } ) ; } ) ; this . $on ( "event-deleted" , ( ) => { return this . $router. push ( { name: RouteName. HOME } ) ; } ) ; } async openDeleteEventModalWrapper ( ) : Promise< void > { await this . openDeleteEventModal ( this . event) ; } async reportEvent ( content: string, forward: boolean) : Promise< void > { this . isReportModalActive = false ; this . $refs. reportModal. close ( ) ; if ( ! this . organizer) return ; const eventTitle = this . event. title; try { await this . $apollo. mutate< IReport> ( { mutation: CREATE_REPORT , variables: { eventId: this . event. id, reportedId: this . organizer ? this . organizer. id : null , content, forward, } , } ) ; this . $notifier. success ( this . $t ( "Event {eventTitle} reported" , { eventTitle } ) as string ) ; } catch ( error) { console. error ( error) ; } } joinEventWithConfirmation ( actor: IPerson) : void { this . isJoinConfirmationModalActive = true ; this . actorForConfirmation = actor; } async joinEvent ( identity: IPerson, message: string | null = null ) : Promise< void > { this . isJoinConfirmationModalActive = false ; this . isJoinModalActive = false ; try { const { data: mutationData } = await this . $apollo. mutate< { joinEvent: IParticipant; } > ( { mutation: JOIN_EVENT , variables: { eventId: this . event. id, actorId: identity. id, message, } , update : ( store: ApolloCache< { joinEvent: IParticipant; } > , { data } : FetchResult ) => { if ( data == null ) return ; const participationCachedData = store. readQuery< { person: IPerson } > ( { query: EVENT_PERSON_PARTICIPATION , variables: { eventId: this . event. id, actorId: identity. id } , } ) ; if ( participationCachedData?. person == undefined ) { console. error ( "Cannot update participation cache, because of null value." ) ; return ; } store. writeQuery ( { query: EVENT_PERSON_PARTICIPATION , variables: { eventId: this . event. id, actorId: identity. id } , data: { person: { ... participationCachedData?. person, participations: { elements: [ data. joinEvent] , total: 1 , } , } , } , } ) ; const cachedData = store. readQuery< { event: IEvent } > ( { query: FETCH_EVENT , variables: { uuid: this . event. uuid } , } ) ; if ( cachedData == null ) return ; const { event } = cachedData; if ( event === null ) { console. error ( "Cannot update event participant cache, because of null value." ) ; return ; } const participantStats = { ... event. participantStats } ; if ( data. joinEvent. role === ParticipantRole. NOT_APPROVED ) { participantStats. notApproved += 1 ; } else { participantStats. going += 1 ; participantStats. participant += 1 ; } store. writeQuery ( { query: FETCH_EVENT , variables: { uuid: this . uuid } , data: { event: { ... event, participantStats, } , } , } ) ; } , } ) ; if ( mutationData) { if ( mutationData. joinEvent. role === ParticipantRole. NOT_APPROVED ) { this . participationRequestedMessage ( ) ; } else { this . participationConfirmedMessage ( ) ; } } } catch ( error) { console. error ( error) ; } } confirmLeave ( ) : void { this . $buefy. dialog. confirm ( { title: this . $t ( 'Leaving event "{title}"' , { title: this . event. title, } ) as string, message: this . $t ( 'Are you sure you want to cancel your participation at event "{title}"?' , { title: this . event. title, } ) as string, confirmText: this . $t ( "Leave event" ) as string, cancelText: this . $t ( "Cancel" ) as string, type: "is-danger" , hasIcon: true , onConfirm : ( ) => { if ( this . currentActor. id) { this . leaveEvent ( this . event, this . currentActor. id) ; } } , } ) ; } @Watch ( "participations" ) watchParticipations ( ) : void { if ( this . participations. length > 0 ) { if ( this . oldParticipationRole && this . participations[ 0 ] . role !== ParticipantRole. NOT_APPROVED && this . oldParticipationRole !== this . participations[ 0 ] . role ) { switch ( this . participations[ 0 ] . role) { case ParticipantRole. PARTICIPANT : this . participationConfirmedMessage ( ) ; break ; case ParticipantRole. REJECTED : this . participationRejectedMessage ( ) ; break ; default : this . participationChangedMessage ( ) ; break ; } } this . oldParticipationRole = this . participations[ 0 ] . role; } } private participationConfirmedMessage ( ) { this . $notifier. success ( this . $t ( "Your participation has been confirmed" ) as string ) ; } private participationRequestedMessage ( ) { this . $notifier. success ( this . $t ( "Your participation has been requested" ) as string ) ; } private participationRejectedMessage ( ) { this . $notifier. error ( this . $t ( "Your participation has been rejected" ) as string ) ; } private participationChangedMessage ( ) { this . $notifier. info ( this . $t ( "Your participation status has been changed" ) as string ) ; } async downloadIcsEvent ( ) : Promise< void > { const data = await ( await fetch ( ` ${ GRAPHQL_API_ENDPOINT } /events/ ${ this . uuid} /export/ics ` ) ) . text ( ) ; const blob = new Blob ( [ data] , { type: "text/calendar" } ) ; const link = document. createElement ( "a" ) ; link. href = window. URL . createObjectURL ( blob) ; link. download = ` ${ this . event. title} .ics ` ; document. body. appendChild ( link) ; link. click ( ) ; document. body. removeChild ( link) ; } triggerShare ( ) : void { if ( navigator. share) { navigator . share ( { title: this . event. title, url: this . event. url, } ) . then ( ( ) => console. log ( "Successful share" ) ) . catch ( ( error: any ) => console. log ( "Error sharing" , error) ) ; } else { this . isShareModalActive = true ; } } handleErrors ( errors: any[ ] ) : void { if ( errors. some ( ( error ) => error. status_code === 404 ) || errors. some ( ( { message } ) => message. includes ( "has invalid value $uuid" ) ) ) { this . $router. replace ( { name: RouteName. PAGE_NOT_FOUND } ) ; } } get actorIsParticipant ( ) : boolean { if ( this . actorIsOrganizer) return true ; return ( this . participations. length > 0 && this . participations[ 0 ] . role === ParticipantRole. PARTICIPANT ) ; } get actorIsOrganizer ( ) : boolean { return ( this . participations. length > 0 && this . participations[ 0 ] . role === ParticipantRole. CREATOR ) ; } get hasGroupPrivileges ( ) : boolean { return ( this . person?. memberships?. total > 0 && [ MemberRole. MODERATOR , MemberRole. ADMINISTRATOR ] . includes ( this . person?. memberships?. elements[ 0 ] . role ) ) ; } get canManageEvent ( ) : boolean { return this . actorIsOrganizer || this . hasGroupPrivileges; } get endDate ( ) : Date { return this . event. endsOn !== null && this . event. endsOn > this . event. beginsOn ? this . event. endsOn : this . event. beginsOn; } get eventCapacityOK ( ) : boolean { if ( this . event. draft) return true ; if ( ! this . event. options. maximumAttendeeCapacity) return true ; return ( this . event. options. maximumAttendeeCapacity > this . event. participantStats. participant ) ; } get numberOfPlacesStillAvailable ( ) : number { if ( this . event. draft) return this . event. options. maximumAttendeeCapacity; return ( this . event. options. maximumAttendeeCapacity - this . event. participantStats. participant ) ; } async anonymousParticipationConfirmed ( ) : Promise< boolean> { return isParticipatingInThisEvent ( this . uuid) ; } async cancelAnonymousParticipation ( ) : Promise< void > { const token = ( await getLeaveTokenForParticipation ( this . uuid) ) as string; await this . leaveEvent ( this . event, this . config. anonymous. actorId, token) ; await removeAnonymousParticipation ( this . uuid) ; this . anonymousParticipation = null ; } get ableToReport ( ) : boolean { return ( this . config && ( this . currentActor. id != null || this . config. anonymous. reports. allowed) ) ; } get organizer ( ) : IActor | null { if ( this . event. attributedTo && this . event. attributedTo. id) { return this . event. attributedTo; } if ( this . event. organizerActor) { return this . event. organizerActor; } return null ; } get organizerDomain ( ) : string | null { if ( this . organizer) { return this . organizer. domain; } return null ; } metadataToComponent: Record< string, string> = { "mz:live:twitch:url" : "integration-twitch" , "mz:live:peertube:url" : "integration-peertube" , "mz:live:youtube:url" : "integration-youtube" , "mz:visio:jitsi_meet" : "integration-jitsi-meet" , "mz:notes:etherpad:url" : "integration-etherpad" , } ; get integrations ( ) : Record< string, IEventMetadataDescription> { return this . event. metadata . map ( ( val ) => { const def = eventMetaDataList. find ( ( dat ) => dat. key === val. key) ; return { ... def, ... val, } ; } ) . reduce ( ( acc: Record< string, IEventMetadataDescription> , metadata ) => { const component = this . metadataToComponent[ metadata. key] ; if ( component !== undefined ) { acc[ component] = metadata; } return acc; } , { } ) ; } } </ script> < style lang = " scss" scoped > .section { padding : 1rem 2rem 4rem; } .fade-enter-active, .fade-leave-active { transition : opacity 0.5s; } .fade-enter, .fade-leave-to { opacity : 0; } div.sidebar { display : flex; flex-wrap : wrap; flex-direction : column; position : relative; &::before { content : "" ; background : #b3b3b2; position : absolute; bottom : 30px; top : 30px; left : 0; height : calc ( 100% - 60px) ; width : 1px; } div.organizer { display : inline-flex; padding-top : 10px; a { color : #4a4a4a; span { line-height : 2.7rem; padding-right : 6px; } } } } .intro { background : white; .is-3-tablet { width : initial; } p.tags { a { text-decoration : none; } span { &.tag { margin : 0 2px; } } } } .event-description-wrapper { display : flex; flex-wrap : wrap; flex-direction : column; padding : 0; @media all and ( min-width : 672px) { flex-direction : row-reverse; } & > aside, & > div { @media all and ( min-width : 672px) { margin : 2rem auto; } } aside.event-metadata { min-width : 20rem; flex : 1; @media all and ( min-width : 672px) { padding-left : 1rem; } .sticky { position : sticky; background : white; top : 50px; padding : 1rem; } } div.event-description-comments { min-width : 20rem; padding : 1rem; flex : 2; background : white; } .description-content { ::v-deep h1 { font-size : 2rem; } ::v-deep h2 { font-size : 1.5rem; } ::v-deep h3 { font-size : 1.25rem; } ::v-deep ul { list-style-type : disc; } ::v-deep li { margin : 10px auto 10px 2rem; } ::v-deep blockquote { border-left : 0.2em solid #333; display : block; padding-left : 1em; } ::v-deep p { margin : 10px auto; a { display : inline-block; padding : 0.3rem; background : $secondary; color : #111; &:empty { display : none; } } } } } .comments { padding-top : 3rem; a h3#comments { margin-bottom : 10px; } } .more-events { background : white; padding : 1rem 1rem 4rem; & > .title { font-size : 1.5rem; } } .dropdown .dropdown-trigger span { cursor : pointer; } a.dropdown-item,.dropdown .dropdown-menu .has-link a, button.dropdown-item { white-space : nowrap; width : 100%; padding-right : 1rem; text-align : right; } a.participations-link { text-decoration : none; } .event-status .tag { font-size : 1rem; } .no-border { border : 0; cursor : auto; } .wrapper, .intro-wrapper { display : flex; flex-direction : column; } .intro-wrapper { position : relative; padding : 0 16px 16px; background : #fff; .date-calendar-icon-wrapper { margin-top : 16px; height : 0; display : flex; align-items : flex-end; align-self : flex-start; margin-bottom : 7px; margin-left : 0rem; } } .title { margin : 0; font-size : 2rem; } </ style>
Hyperlink with router 5/5 - GraphQL query with variable
The named variable $uuid
from the component's prop can be used for GraphQL query.
Browse code : js/src/graphql/event.ts
Reveal code
import gql from "graphql-tag" ; import { ADDRESS_FRAGMENT } from "./address" ; import { TAG_FRAGMENT } from "./tags" ; const PARTICIPANT_QUERY_FRAGMENT = gql` fragment ParticipantQuery on Participant { role id actor { preferredUsername avatar { id url } name id domain } event { id uuid } metadata { cancellationToken message } insertedAt } ` ; const PARTICIPANTS_QUERY_FRAGMENT = gql` fragment ParticipantsQuery on PaginatedParticipantList { total elements { ...ParticipantQuery } } ${ PARTICIPANT_QUERY_FRAGMENT } ` ; const EVENT_OPTIONS_FRAGMENT = gql` fragment EventOptions on EventOptions { maximumAttendeeCapacity remainingAttendeeCapacity showRemainingAttendeeCapacity anonymousParticipation showStartTime showEndTime offers { price priceCurrency url } participationConditions { title content url } attendees program commentModeration showParticipationPrice hideOrganizerWhenGroupEvent } ` ; const FULL_EVENT_FRAGMENT = gql` fragment FullEvent on Event { id uuid url local title description beginsOn endsOn status visibility joinOptions draft picture { id url name metadata { width height blurhash } } publishAt onlineAddress phoneAddress physicalAddress { ...AdressFragment } organizerActor { avatar { id url } preferredUsername domain name url id summary } contacts { avatar { id url } preferredUsername name summary domain url id } attributedTo { avatar { id url } preferredUsername name summary domain url id } participantStats { going notApproved participant } tags { ...TagFragment } relatedEvents { id uuid title beginsOn picture { id url name metadata { width height blurhash } } physicalAddress { id description } organizerActor { id avatar { id url } preferredUsername domain name } } options { ...EventOptions } metadata { key title value type } } ${ ADDRESS_FRAGMENT } ${ TAG_FRAGMENT } ${ EVENT_OPTIONS_FRAGMENT } ` ; export const FETCH_EVENT = gql` query FetchEvent($uuid: UUID!) { event(uuid: $uuid) { ...FullEvent } } ${ FULL_EVENT_FRAGMENT } ` ; export const FETCH_EVENT_BASIC = gql` query ($uuid: UUID!) { event(uuid: $uuid) { id uuid joinOptions participantStats { going notApproved notConfirmed participant } } } ` ; export const FETCH_EVENTS = gql` query FetchEvents( $orderBy: EventOrderBy $direction: SortDirection $page: Int $limit: Int ) { events( orderBy: $orderBy direction: $direction page: $page limit: $limit ) { total elements { id uuid url local title description beginsOn endsOn status visibility insertedAt picture { id url } publishAt # online_address, # phone_address, physicalAddress { id description locality } organizerActor { id avatar { id url } preferredUsername domain name } attributedTo { avatar { id url } preferredUsername name } category tags { ...TagFragment } } } } ${ TAG_FRAGMENT } ` ; export const CREATE_EVENT = gql` mutation createEvent( $organizerActorId: ID! $attributedToId: ID $title: String! $description: String! $beginsOn: DateTime! $endsOn: DateTime $status: EventStatus $visibility: EventVisibility $joinOptions: EventJoinOptions $draft: Boolean $tags: [String] $picture: MediaInput $onlineAddress: String $phoneAddress: String $category: String $physicalAddress: AddressInput $options: EventOptionsInput $contacts: [Contact] ) { createEvent( organizerActorId: $organizerActorId attributedToId: $attributedToId title: $title description: $description beginsOn: $beginsOn endsOn: $endsOn status: $status visibility: $visibility joinOptions: $joinOptions draft: $draft tags: $tags picture: $picture onlineAddress: $onlineAddress phoneAddress: $phoneAddress category: $category physicalAddress: $physicalAddress options: $options contacts: $contacts ) { ...FullEvent } } ${ FULL_EVENT_FRAGMENT } ` ; export const EDIT_EVENT = gql` mutation updateEvent( $id: ID! $title: String $description: String $beginsOn: DateTime $endsOn: DateTime $status: EventStatus $visibility: EventVisibility $joinOptions: EventJoinOptions $draft: Boolean $tags: [String] $picture: MediaInput $onlineAddress: String $phoneAddress: String $organizerActorId: ID $attributedToId: ID $category: String $physicalAddress: AddressInput $options: EventOptionsInput $contacts: [Contact] $metadata: EventMetadataInput ) { updateEvent( eventId: $id title: $title description: $description beginsOn: $beginsOn endsOn: $endsOn status: $status visibility: $visibility joinOptions: $joinOptions draft: $draft tags: $tags picture: $picture onlineAddress: $onlineAddress phoneAddress: $phoneAddress organizerActorId: $organizerActorId attributedToId: $attributedToId category: $category physicalAddress: $physicalAddress options: $options contacts: $contacts metadata: $metadata ) { ...FullEvent } } ${ FULL_EVENT_FRAGMENT } ` ; export const JOIN_EVENT = gql` mutation JoinEvent( $eventId: ID! $actorId: ID! $email: String $message: String $locale: String ) { joinEvent( eventId: $eventId actorId: $actorId email: $email message: $message locale: $locale ) { ...ParticipantQuery } } ${ PARTICIPANT_QUERY_FRAGMENT } ` ; export const LEAVE_EVENT = gql` mutation LeaveEvent($eventId: ID!, $actorId: ID!, $token: String) { leaveEvent(eventId: $eventId, actorId: $actorId, token: $token) { actor { id } } } ` ; export const CONFIRM_PARTICIPATION = gql` mutation ConfirmParticipation($token: String!) { confirmParticipation(confirmationToken: $token) { actor { id } event { id uuid joinOptions } role } } ` ; export const UPDATE_PARTICIPANT = gql` mutation UpdateParticipant($id: ID!, $role: ParticipantRoleEnum!) { updateParticipation(id: $id, role: $role) { role id } } ` ; export const DELETE_EVENT = gql` mutation DeleteEvent($eventId: ID!) { deleteEvent(eventId: $eventId) { id } } ` ; export const PARTICIPANTS = gql` query Participants($uuid: UUID!, $page: Int, $limit: Int, $roles: String) { event(uuid: $uuid) { id uuid title participants(page: $page, limit: $limit, roles: $roles) { ...ParticipantsQuery } participantStats { going notApproved rejected participant } } } ${ PARTICIPANTS_QUERY_FRAGMENT } ` ; export const EVENT_PERSON_PARTICIPATION = gql` query EventPersonParticipation($actorId: ID!, $eventId: ID!) { person(id: $actorId) { id participations(eventId: $eventId) { total elements { id role actor { id } event { id } } } } } ` ; export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql` subscription EventPersonParticipationSubscriptionChanged( $actorId: ID! $eventId: ID! ) { eventPersonParticipationChanged(personId: $actorId) { id participations(eventId: $eventId) { total elements { id role actor { id } event { id } } } } } ` ; export const FETCH_GROUP_EVENTS = gql` query FetchGroupEvents( $name: String! $afterDateTime: DateTime $beforeDateTime: DateTime $organisedEventsPage: Int $organisedEventslimit: Int ) { group(preferredUsername: $name) { id preferredUsername domain name organizedEvents( afterDatetime: $afterDateTime beforeDatetime: $beforeDateTime page: $organisedEventsPage limit: $organisedEventslimit ) { elements { id uuid title beginsOn draft options { maximumAttendeeCapacity } participantStats { participant notApproved } attributedTo { id preferredUsername name domain } organizerActor { id preferredUsername name domain } } total } } } ` ; export const CLOSE_EVENTS = gql` query CloseEvents($location: String, $radius: Float) { searchEvents(location: $location, radius: $radius, page: 1, limit: 10) { total elements { id title uuid beginsOn picture { id url } tags { slug title } __typename } } } ` ;