]> Untitled Git - lemmy.git/blobdiff - ui/src/utils.ts
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / utils.ts
index 480b41c7c313a6ffcd6403456583b4ee14fe2ee9..a312a663c5ced1fc8e59d5420502a2c5be65330d 100644 (file)
@@ -1,4 +1,6 @@
 import 'moment/locale/es';
+import 'moment/locale/el';
+import 'moment/locale/eu';
 import 'moment/locale/eo';
 import 'moment/locale/de';
 import 'moment/locale/zh-cn';
@@ -10,9 +12,20 @@ import 'moment/locale/it';
 import 'moment/locale/fi';
 import 'moment/locale/ca';
 import 'moment/locale/fa';
+import 'moment/locale/pl';
 import 'moment/locale/pt-br';
 import 'moment/locale/ja';
 import 'moment/locale/ka';
+import 'moment/locale/hi';
+import 'moment/locale/gl';
+import 'moment/locale/tr';
+import 'moment/locale/hu';
+import 'moment/locale/uk';
+import 'moment/locale/sq';
+import 'moment/locale/km';
+import 'moment/locale/ga';
+import 'moment/locale/sr';
+import 'moment/locale/ko';
 
 import {
   UserOperation,
@@ -22,9 +35,7 @@ import {
   PrivateMessage,
   User,
   SortType,
-  CommentSortType,
   ListingType,
-  DataType,
   SearchType,
   WebSocketResponse,
   WebSocketJsonResponse,
@@ -32,24 +43,31 @@ import {
   SearchResponse,
   CommentResponse,
   PostResponse,
-} from './interfaces';
+} from 'lemmy-js-client';
+
+import { CommentSortType, DataType } from './interfaces';
 import { UserService, WebSocketService } from './services';
 
 import Tribute from 'tributejs/src/Tribute.js';
 import markdown_it from 'markdown-it';
+import markdown_it_sub from 'markdown-it-sub';
+import markdown_it_sup from 'markdown-it-sup';
 import markdownitEmoji from 'markdown-it-emoji/light';
 import markdown_it_container from 'markdown-it-container';
-import twemoji from 'twemoji';
 import emojiShortName from 'emoji-short-name';
 import Toastify from 'toastify-js';
 import tippy from 'tippy.js';
-import EmojiButton from '@joeattardi/emoji-button';
+import moment from 'moment';
 
+export const favIconUrl = '/static/assets/favicon.svg';
+export const favIconPngUrl = '/static/assets/apple-touch-icon.png';
+export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
 export const repoUrl = 'https://github.com/LemmyNet/lemmy';
 export const helpGuideUrl = '/docs/about_guide.html';
 export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
 export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
 export const archiveUrl = 'https://archive.is';
+export const elementUrl = 'https://element.io/';
 
 export const postRefetchSeconds: number = 60 * 1000;
 export const fetchLimit: number = 20;
@@ -58,17 +76,30 @@ export const mentionDropdownFetchLimit = 10;
 export const languages = [
   { code: 'ca', name: 'Català' },
   { code: 'en', name: 'English' },
+  { code: 'el', name: 'Ελληνικά' },
+  { code: 'eu', name: 'Euskara' },
   { code: 'eo', name: 'Esperanto' },
   { code: 'es', name: 'Español' },
   { code: 'de', name: 'Deutsch' },
+  { code: 'ga', name: 'Gaeilge' },
+  { code: 'gl', name: 'Galego' },
+  { code: 'hu', name: 'Magyar Nyelv' },
   { code: 'ka', name: 'ქართული ენა' },
+  { code: 'ko', name: '한국어' },
+  { code: 'km', name: 'ភាសាខ្មែរ' },
+  { code: 'hi', name: 'मानक हिन्दी' },
   { code: 'fa', name: 'فارسی' },
   { code: 'ja', name: '日本語' },
+  { code: 'pl', name: 'Polski' },
   { code: 'pt_BR', name: 'Português Brasileiro' },
   { code: 'zh', name: '中文' },
   { code: 'fi', name: 'Suomi' },
   { code: 'fr', name: 'Français' },
   { code: 'sv', name: 'Svenska' },
+  { code: 'sq', name: 'Shqip' },
+  { code: 'sr_Latn', name: 'srpski' },
+  { code: 'tr', name: 'Türkçe' },
+  { code: 'uk', name: 'Українська Mова' },
   { code: 'ru', name: 'Русский' },
   { code: 'nl', name: 'Nederlands' },
   { code: 'it', name: 'Italiano' },
@@ -87,21 +118,29 @@ export const themes = [
   'vaporwave',
   'vaporwave-dark',
   'i386',
+  'litely',
 ];
 
-export const emojiPicker = new EmojiButton({
-  // Use the emojiShortName from native
-  style: 'twemoji',
-  theme: 'dark',
-  position: 'auto-start',
-  // TODO i18n
-});
+const DEFAULT_ALPHABET =
+  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 
-export function randomStr() {
-  return Math.random()
-    .toString(36)
-    .replace(/[^a-z]+/g, '')
-    .substr(2, 10);
+function getRandomCharFromAlphabet(alphabet: string): string {
+  return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
+}
+
+export function randomStr(
+  idDesiredLength: number = 20,
+  alphabet = DEFAULT_ALPHABET
+): string {
+  /**
+   * Create n-long array and map it to random chars from given alphabet.
+   * Then join individual chars as string
+   */
+  return Array.from({ length: idDesiredLength })
+    .map(() => {
+      return getRandomCharFromAlphabet(alphabet);
+    })
+    .join('');
 }
 
 export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
@@ -117,6 +156,8 @@ export const md = new markdown_it({
   linkify: true,
   typographer: true,
 })
+  .use(markdown_it_sub)
+  .use(markdown_it_sup)
   .use(markdown_it_container, 'spoiler', {
     validate: function (params: any) {
       return params.trim().match(/^spoiler\s+(.*)$/);
@@ -138,10 +179,6 @@ export const md = new markdown_it({
     defs: objectFlip(emojiShortName),
   });
 
-md.renderer.rules.emoji = function (token, idx) {
-  return twemoji.parse(token[idx].content);
-};
-
 export function hotRankComment(comment: Comment): number {
   return hotRank(comment.score, comment.published);
 }
@@ -208,7 +245,7 @@ export function isMod(modIds: Array<number>, creator_id: number): boolean {
 }
 
 const imageRegex = new RegExp(
-  /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg))/
+  /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/
 );
 const videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
 
@@ -238,25 +275,11 @@ export function capitalizeFirstLetter(str: string): string {
 }
 
 export function routeSortTypeToEnum(sort: string): SortType {
-  if (sort == 'new') {
-    return SortType.New;
-  } else if (sort == 'hot') {
-    return SortType.Hot;
-  } else if (sort == 'topday') {
-    return SortType.TopDay;
-  } else if (sort == 'topweek') {
-    return SortType.TopWeek;
-  } else if (sort == 'topmonth') {
-    return SortType.TopMonth;
-  } else if (sort == 'topyear') {
-    return SortType.TopYear;
-  } else if (sort == 'topall') {
-    return SortType.TopAll;
-  }
+  return SortType[sort];
 }
 
 export function routeListingTypeToEnum(type: string): ListingType {
-  return ListingType[capitalizeFirstLetter(type)];
+  return ListingType[type];
 }
 
 export function routeDataTypeToEnum(type: string): DataType {
@@ -319,9 +342,9 @@ export function debounce(
   };
 }
 
-export function getLanguage(): string {
+export function getLanguage(override?: string): string {
   let user = UserService.Instance.user;
-  let lang = user && user.lang ? user.lang : 'browser';
+  let lang = override || (user && user.lang ? user.lang : 'browser');
 
   if (lang == 'browser') {
     return getBrowserLanguage();
@@ -360,19 +383,45 @@ export function getMomentLanguage(): string {
     lang = 'ca';
   } else if (lang.startsWith('fa')) {
     lang = 'fa';
+  } else if (lang.startsWith('pl')) {
+    lang = 'pl';
   } else if (lang.startsWith('pt')) {
     lang = 'pt-br';
   } else if (lang.startsWith('ja')) {
     lang = 'ja';
   } else if (lang.startsWith('ka')) {
     lang = 'ka';
+  } else if (lang.startsWith('hi')) {
+    lang = 'hi';
+  } else if (lang.startsWith('el')) {
+    lang = 'el';
+  } else if (lang.startsWith('eu')) {
+    lang = 'eu';
+  } else if (lang.startsWith('gl')) {
+    lang = 'gl';
+  } else if (lang.startsWith('tr')) {
+    lang = 'tr';
+  } else if (lang.startsWith('hu')) {
+    lang = 'hu';
+  } else if (lang.startsWith('uk')) {
+    lang = 'uk';
+  } else if (lang.startsWith('sq')) {
+    lang = 'sq';
+  } else if (lang.startsWith('km')) {
+    lang = 'km';
+  } else if (lang.startsWith('ga')) {
+    lang = 'ga';
+  } else if (lang.startsWith('sr')) {
+    lang = 'sr';
+  } else if (lang.startsWith('ko')) {
+    lang = 'ko';
   } else {
     lang = 'en';
   }
   return lang;
 }
 
-export function setTheme(theme: string = 'darkly') {
+export function setTheme(theme: string = 'darkly', loggedIn: boolean = false) {
   // unload all the other themes
   for (var i = 0; i < themes.length; i++) {
     let styleSheet = document.getElementById(themes[i]);
@@ -381,18 +430,36 @@ export function setTheme(theme: string = 'darkly') {
     }
   }
 
-  // Load the theme dynamically
-  if (!document.getElementById(theme)) {
+  // if the user is not logged in, we load the default themes and let the browser decide
+  if (!loggedIn) {
+    document.getElementById('default-light').removeAttribute('disabled');
+    document.getElementById('default-dark').removeAttribute('disabled');
+  } else {
+    document
+      .getElementById('default-light')
+      .setAttribute('disabled', 'disabled');
+    document
+      .getElementById('default-dark')
+      .setAttribute('disabled', 'disabled');
+
+    // Load the theme dynamically
+    let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
+    loadCss(theme, cssLoc);
+    document.getElementById(theme).removeAttribute('disabled');
+  }
+}
+
+export function loadCss(id: string, loc: string) {
+  if (!document.getElementById(id)) {
     var head = document.getElementsByTagName('head')[0];
     var link = document.createElement('link');
-    link.id = theme;
+    link.id = id;
     link.rel = 'stylesheet';
     link.type = 'text/css';
-    link.href = `/static/assets/css/themes/${theme}.min.css`;
+    link.href = loc;
     link.media = 'all';
     head.appendChild(link);
   }
-  document.getElementById(theme).removeAttribute('disabled');
 }
 
 export function objectFlip(obj: any) {
@@ -403,10 +470,12 @@ export function objectFlip(obj: any) {
   return ret;
 }
 
-export function pictshareAvatarThumbnail(src: string): string {
-  // sample url: http://localhost:8535/pictshare/gs7xuu.jpg
-  let split = src.split('pictshare');
-  let out = `${split[0]}pictshare/96${split[1]}`;
+export function pictrsAvatarThumbnail(src: string): string {
+  // sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
+  let split = src.split('/pictrs/image');
+  let out = `${split[0]}/pictrs/image/${
+    canUseWebP() ? 'webp/' : ''
+  }thumbnail96${split[1]}`;
   return out;
 }
 
@@ -417,26 +486,49 @@ export function showAvatars(): boolean {
   );
 }
 
+export function isCakeDay(published: string): boolean {
+  // moment(undefined) or moment.utc(undefined) returns the current date/time
+  // moment(null) or moment.utc(null) returns null
+  const userCreationDate = moment.utc(published || null).local();
+  const currentDate = moment(new Date());
+
+  return (
+    userCreationDate.date() === currentDate.date() &&
+    userCreationDate.month() === currentDate.month() &&
+    userCreationDate.year() !== currentDate.year()
+  );
+}
+
 // Converts to image thumbnail
-export function pictshareImage(
-  hash: string,
-  thumbnail: boolean = false
-): string {
-  let root = `/pictshare`;
+export function pictrsImage(hash: string, thumbnail: boolean = false): string {
+  let root = `/pictrs/image`;
 
   // Necessary for other servers / domains
-  if (hash.includes('pictshare')) {
-    let split = hash.split('/pictshare/');
-    root = `${split[0]}/pictshare`;
+  if (hash.includes('pictrs')) {
+    let split = hash.split('/pictrs/image/');
+    root = `${split[0]}/pictrs/image`;
     hash = split[1];
   }
 
-  let out = `${root}/${thumbnail ? '192/' : ''}${hash}`;
+  let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
+    thumbnail ? 'thumbnail256/' : ''
+  }${hash}`;
   return out;
 }
 
-export function isCommentType(item: Comment | PrivateMessage): item is Comment {
-  return (item as Comment).community_id !== undefined;
+export function isCommentType(
+  item: Comment | PrivateMessage | Post
+): item is Comment {
+  return (
+    (item as Comment).community_id !== undefined &&
+    (item as Comment).content !== undefined
+  );
+}
+
+export function isPostType(
+  item: Comment | PrivateMessage | Post
+): item is Post {
+  return (item as Post).stickied !== undefined;
 }
 
 export function toast(text: string, background: string = 'success') {
@@ -449,42 +541,117 @@ export function toast(text: string, background: string = 'success') {
   }).showToast();
 }
 
-export function messageToastify(
-  creator: string,
-  avatar: string,
-  body: string,
-  link: string,
-  router: any
+export function pictrsDeleteToast(
+  clickToDeleteText: string,
+  deletePictureText: string,
+  deleteUrl: string
 ) {
   let backgroundColor = `var(--light)`;
+  let toast = Toastify({
+    text: clickToDeleteText,
+    backgroundColor: backgroundColor,
+    gravity: 'top',
+    position: 'right',
+    duration: 10000,
+    onClick: () => {
+      if (toast) {
+        window.location.replace(deleteUrl);
+        alert(deletePictureText);
+        toast.hideToast();
+      }
+    },
+    close: true,
+  }).showToast();
+}
+
+interface NotifyInfo {
+  name: string;
+  icon: string;
+  link: string;
+  body: string;
+}
+
+export function messageToastify(info: NotifyInfo, router: any) {
+  let htmlBody = info.body ? md.render(info.body) : '';
+  let backgroundColor = `var(--light)`;
 
   let toast = Toastify({
-    text: `${body}<br />${creator}`,
-    avatar: avatar,
+    text: `${htmlBody}<br />${info.name}`,
+    avatar: info.icon,
     backgroundColor: backgroundColor,
+    className: 'text-dark',
     close: true,
     gravity: 'top',
     position: 'right',
-    duration: 0,
+    duration: 5000,
     onClick: () => {
       if (toast) {
         toast.hideToast();
-        router.history.push(link);
+        router.history.push(info.link);
       }
     },
   }).showToast();
 }
 
+export function notifyPost(post: Post, router: any) {
+  let info: NotifyInfo = {
+    name: post.community_name,
+    icon: post.community_icon ? post.community_icon : defaultFavIcon,
+    link: `/post/${post.id}`,
+    body: post.name,
+  };
+  notify(info, router);
+}
+
+export function notifyComment(comment: Comment, router: any) {
+  let info: NotifyInfo = {
+    name: comment.creator_name,
+    icon: comment.creator_avatar ? comment.creator_avatar : defaultFavIcon,
+    link: `/post/${comment.post_id}/comment/${comment.id}`,
+    body: comment.content,
+  };
+  notify(info, router);
+}
+
+export function notifyPrivateMessage(pm: PrivateMessage, router: any) {
+  let info: NotifyInfo = {
+    name: pm.creator_name,
+    icon: pm.creator_avatar ? pm.creator_avatar : defaultFavIcon,
+    link: `/inbox`,
+    body: pm.content,
+  };
+  notify(info, router);
+}
+
+function notify(info: NotifyInfo, router: any) {
+  messageToastify(info, router);
+
+  if (Notification.permission !== 'granted') Notification.requestPermission();
+  else {
+    var notification = new Notification(info.name, {
+      icon: info.icon,
+      body: info.body,
+    });
+
+    notification.onclick = () => {
+      event.preventDefault();
+      router.history.push(info.link);
+    };
+  }
+}
+
 export function setupTribute(): Tribute {
   return new Tribute({
+    noMatchTemplate: function () {
+      return '';
+    },
     collection: [
       // Emojis
       {
         trigger: ':',
         menuItemTemplate: (item: any) => {
           let shortName = `:${item.original.key}:`;
-          let twemojiIcon = twemoji.parse(item.original.val);
-          return `${twemojiIcon} ${shortName}`;
+          return `${item.original.val} ${shortName}`;
         },
         selectTemplate: (item: any) => {
           return `:${item.original.key}:`;
@@ -501,7 +668,10 @@ export function setupTribute(): Tribute {
       {
         trigger: '@',
         selectTemplate: (item: any) => {
-          return `[/u/${item.original.key}](/u/${item.original.key})`;
+          let link = item.original.local
+            ? `[${item.original.key}](/u/${item.original.name})`
+            : `[${item.original.key}](/user/${item.original.id})`;
+          return link;
         },
         values: (text: string, cb: any) => {
           userSearch(text, (users: any) => cb(users));
@@ -514,9 +684,12 @@ export function setupTribute(): Tribute {
 
       // Communities
       {
-        trigger: '#',
+        trigger: '!',
         selectTemplate: (item: any) => {
-          return `[/c/${item.original.key}](/c/${item.original.key})`;
+          let link = item.original.local
+            ? `[${item.original.key}](/c/${item.original.name})`
+            : `[${item.original.key}](/community/${item.original.id})`;
+          return link;
         },
         values: (text: string, cb: any) => {
           communitySearch(text, (communities: any) => cb(communities));
@@ -545,24 +718,29 @@ function userSearch(text: string, cb: any) {
   if (text) {
     let form: SearchForm = {
       q: text,
-      type_: SearchType[SearchType.Users],
-      sort: SortType[SortType.TopAll],
+      type_: SearchType.Users,
+      sort: SortType.TopAll,
       page: 1,
       limit: mentionDropdownFetchLimit,
     };
 
     WebSocketService.Instance.search(form);
 
-    this.userSub = WebSocketService.Instance.subject.subscribe(
+    let userSub = WebSocketService.Instance.subject.subscribe(
       msg => {
         let res = wsJsonToRes(msg);
         if (res.op == UserOperation.Search) {
           let data = res.data as SearchResponse;
           let users = data.users.map(u => {
-            return { key: u.name };
+            return {
+              key: `@${u.name}@${hostname(u.actor_id)}`,
+              name: u.name,
+              local: u.local,
+              id: u.id,
+            };
           });
           cb(users);
-          this.userSub.unsubscribe();
+          userSub.unsubscribe();
         }
       },
       err => console.error(err),
@@ -577,24 +755,29 @@ function communitySearch(text: string, cb: any) {
   if (text) {
     let form: SearchForm = {
       q: text,
-      type_: SearchType[SearchType.Communities],
-      sort: SortType[SortType.TopAll],
+      type_: SearchType.Communities,
+      sort: SortType.TopAll,
       page: 1,
       limit: mentionDropdownFetchLimit,
     };
 
     WebSocketService.Instance.search(form);
 
-    this.communitySub = WebSocketService.Instance.subject.subscribe(
+    let communitySub = WebSocketService.Instance.subject.subscribe(
       msg => {
         let res = wsJsonToRes(msg);
         if (res.op == UserOperation.Search) {
           let data = res.data as SearchResponse;
-          let communities = data.communities.map(u => {
-            return { key: u.name };
+          let communities = data.communities.map(c => {
+            return {
+              key: `!${c.name}@${hostname(c.actor_id)}`,
+              name: c.name,
+              local: c.local,
+              id: c.id,
+            };
           });
           cb(communities);
-          this.communitySub.unsubscribe();
+          communitySub.unsubscribe();
         }
       },
       err => console.error(err),
@@ -609,7 +792,7 @@ export function getListingTypeFromProps(props: any): ListingType {
   return props.match.params.listing_type
     ? routeListingTypeToEnum(props.match.params.listing_type)
     : UserService.Instance.user
-    ? UserService.Instance.user.default_listing_type
+    ? Object.values(ListingType)[UserService.Instance.user.default_listing_type]
     : ListingType.All;
 }
 
@@ -624,8 +807,8 @@ export function getSortTypeFromProps(props: any): SortType {
   return props.match.params.sort
     ? routeSortTypeToEnum(props.match.params.sort)
     : UserService.Instance.user
-    ? UserService.Instance.user.default_sort_type
-    : SortType.Hot;
+    ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
+    : SortType.Active;
 }
 
 export function getPageFromProps(props: any): number {
@@ -703,6 +886,11 @@ export function editPostRes(data: PostResponse, post: Post) {
     post.url = data.post.url;
     post.name = data.post.name;
     post.nsfw = data.post.nsfw;
+    post.deleted = data.post.deleted;
+    post.removed = data.post.removed;
+    post.stickied = data.post.stickied;
+    post.body = data.post.body;
+    post.locked = data.post.locked;
   }
 }
 
@@ -771,7 +959,7 @@ function convertCommentSortType(sort: SortType): CommentSortType {
     return CommentSortType.Top;
   } else if (sort == SortType.New) {
     return CommentSortType.New;
-  } else if (sort == SortType.Hot) {
+  } else if (sort == SortType.Hot || sort == SortType.Active) {
     return CommentSortType.Hot;
   } else {
     return CommentSortType.Hot;
@@ -812,7 +1000,15 @@ export function postSort(
         +a.removed - +b.removed ||
         +a.deleted - +b.deleted ||
         (communityType && +b.stickied - +a.stickied) ||
-        hotRankPost(b) - hotRankPost(a)
+        b.hot_rank - a.hot_rank
+    );
+  } else if (sort == SortType.Active) {
+    posts.sort(
+      (a, b) =>
+        +a.removed - +b.removed ||
+        +a.deleted - +b.deleted ||
+        (communityType && +b.stickied - +a.stickied) ||
+        b.hot_rank_active - a.hot_rank_active
     );
   }
 }
@@ -834,14 +1030,63 @@ function randomHsl() {
   return `hsla(${Math.random() * 360}, 100%, 50%, 1)`;
 }
 
-export function previewLines(text: string, lines: number = 3): string {
-  // Use lines * 2 because markdown requires 2 lines
-  return text
-    .split('\n')
-    .slice(0, lines * 2)
-    .join('\n');
+export function previewLines(
+  text: string,
+  maxChars: number = 300,
+  maxLines: number = 1
+): string {
+  return (
+    text
+      .slice(0, maxChars)
+      .split('\n')
+      // Use lines * 2 because markdown requires 2 lines
+      .slice(0, maxLines * 2)
+      .join('\n') + '...'
+  );
 }
 
 export function hostname(url: string): string {
-  return new URL(url).hostname;
+  let cUrl = new URL(url);
+  return window.location.port
+    ? `${cUrl.hostname}:${cUrl.port}`
+    : `${cUrl.hostname}`;
+}
+
+function canUseWebP() {
+  // TODO pictshare might have a webp conversion bug, try disabling this
+  return false;
+
+  // var elem = document.createElement('canvas');
+  // if (!!(elem.getContext && elem.getContext('2d'))) {
+  //   var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp';
+  //   // was able or not to get WebP representation
+  //   return (
+  //     elem.toDataURL('image/webp').startsWith('data:image/' + testString)
+  //   );
+  // }
+
+  // // very old browser like IE 8, canvas not supported
+  // return false;
+}
+
+export function validTitle(title?: string): boolean {
+  // Initial title is null, minimum length is taken care of by textarea's minLength={3}
+  if (title === null || title.length < 3) return true;
+
+  const regex = new RegExp(/.*\S.*/, 'g');
+
+  return regex.test(title);
+}
+
+export function siteBannerCss(banner: string): string {
+  return ` \
+    background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
+    background-attachment: fixed; \
+    background-position: top; \
+    background-repeat: no-repeat; \
+    background-size: 100% cover; \
+
+    width: 100%; \
+    max-height: 100vh; \
+    `;
 }