Skip to content
Snippets Groups Projects
Commit be8ee93a authored by Stefan Galinski's avatar Stefan Galinski :video_game:
Browse files

Merge branch 'task_bootstrap5' into 'master'

Task Bootstrap 5

See merge request !10
parents 7e7948af 8ffef036
No related branches found
Tags 4.1.6
1 merge request!10Task Bootstrap 5
<?php
/***************************************************************
* Copyright notice
*
* (c) sgalinski Internet Services (https://www.sgalinski.de)
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
namespace SGalinski\SgVimeo\ViewHelpers;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
/**
* Use this ViewHelper to get the opposite of the f:format.crop result
*/
final class CropOppositeViewHelper extends AbstractViewHelper {
use CompileWithRenderStatic;
/**
* The output may contain HTML and can not be escaped.
*
* @var bool
*/
protected $escapeOutput = FALSE;
public function initializeArguments(): void {
$this->registerArgument('maxCharacters', 'int', 'Place where to truncate the string', TRUE);
$this->registerArgument('append', 'string', 'What to append, if truncation happened', FALSE, '&hellip;');
$this->registerArgument(
'respectWordBoundaries', 'bool',
'If TRUE and division is in the middle of a word, the remains of that word is removed.', FALSE, TRUE
);
}
public static function renderStatic(
array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext
): string {
$maxCharacters = (int) $arguments['maxCharacters'];
$append = (string) $arguments['append'];
$respectWordBoundaries = (bool) ($arguments['respectWordBoundaries']);
$stringToTruncate = (string) $renderChildrenClosure();
return self::crop(
content: $stringToTruncate,
numberOfChars: $maxCharacters,
replacementForEllipsis: $append,
cropToSpace: $respectWordBoundaries
);
}
/**
* Implements "cropHTML" which is a modified "substr" function allowing to limit a string length to a certain number
* of chars (from either start or end of string) and having a pre/postfix applied if the string really was cropped.
*
* Note: Crop is done without properly respecting html tags and entities.
*
* @param string $content The string to perform the operation on
* @param int $numberOfChars Max number of chars of the string. Negative value means cropping from end of string.
* @param string $replacementForEllipsis The pre/postfix string to apply if cropping occurs.
* @param bool $cropToSpace If true then crop will be applied at nearest space.
* @return string The processed input value.
*/
private static function crop(string $content, int $numberOfChars, string $replacementForEllipsis, bool $cropToSpace
): string {
if (!$numberOfChars || $numberOfChars < 1 || !(mb_strlen($content, 'utf-8') > abs($numberOfChars))) {
return $content;
}
// cropping from the left side of content, appending replacementForEllipsis
$croppedContent = mb_substr($content, 0, $numberOfChars, 'utf-8');
$truncatePosition = $cropToSpace ? mb_strrpos($croppedContent, ' ', 0, 'utf-8') : FALSE;
$cropResult = $truncatePosition > 0
? mb_substr($content, 0, $truncatePosition, 'utf-8')
: $content;
return str_replace($cropResult, '', $content);
}
}
......@@ -23,3 +23,9 @@ plugin.tx_sgvimeo {
}
}
}
[{$plugin.tx_project_theme.config.bootstrapVersion} == 5]
plugin.tx_sgvimeo.view.templateRootPaths.10 = EXT:sg_vimeo/Resources/Private/Templates/Bootstrap5
plugin.tx_sgvimeo.view.partialRootPaths.10 = EXT:sg_vimeo/Resources/Private/Partials/Bootstrap5
plugin.tx_sgvimeo.view.layoutRootPaths.10 = EXT:sg_vimeo/Resources/Private/Layouts/Bootstrap5
[end]
......@@ -14,15 +14,15 @@ First install the extension and activate it in the Extension Manager.
## Vimeo app
* Create a new vimeo app: https://developer.vimeo.com/apps/new
* Answer the question `Will people besides you be able to access your app?` with:
`No. The only Vimeo accounts that will have access to the app are my own.`
* Go to your vimeo app on https://developer.vimeo.com/apps and copy your client identifier & client secret.
- Create a new vimeo app: https://developer.vimeo.com/apps/new
- Answer the question `Will people besides you be able to access your app?` with:
`No. The only Vimeo accounts that will have access to the app are my own.`
- Go to your vimeo app on https://developer.vimeo.com/apps and copy your client identifier & client secret.
### TypoScript integration
* Include the TypoScript in Configuration/TypoScript/setup.typoscript and constants.typoscript in your theme.
* Add your Vimeo client identifier & client secret (and optionally your personal access token):
- Include the TypoScript in Configuration/TypoScript/setup.typoscript and constants.typoscript in your theme.
- Add your Vimeo client identifier & client secret (and optionally your personal access token):
```typoscript
plugin.tx_sgvimeo {
......@@ -45,10 +45,10 @@ https://vimeo.zendesk.com/hc/en-us/articles/360042445032-How-do-I-generate-a-per
The following requirements have to be met, for the private video to show up:
* the video has to be hosted on the same vimeo account, that was used to configure the clientSecret & clientId (vimeo
app)
* a personal access token with access scope "private" has to be configured in the typoscript settings (
personalAccessToken)
- the video has to be hosted on the same vimeo account, that was used to configure the clientSecret & clientId (vimeo
app)
- a personal access token with access scope "private" has to be configured in the typoscript settings (
personalAccessToken)
### Working with Rate Limits
......@@ -57,8 +57,8 @@ API requests (the owner of the vimeo app).
sg_vimeo uses the following endpoints:
* Video - api.vimeo.com/videos/{video_id}
* Channel Videos - api.vimeo.com/channels/{channel_id}/videos
- Video - api.vimeo.com/videos/{video_id}
- Channel Videos - api.vimeo.com/channels/{channel_id}/videos
sg_vimeo uses [field filtering](https://developer.vimeo.com/guidelines/rate-limiting#avoid-rate-limiting) to request
only the fields that are needed.
......@@ -83,8 +83,8 @@ We are shipping the extension with source files and already minified assets. By
we the minified assets are loaded in the Layout, so that the extension works out of the box just with plug and play.
Should you want to change this behavior, you can do the following:
- Override the layout path in TypoScript
local/project_theme/Configuration/TypoScript/Extensions/SgVimeo/Constants.typoscript
- Override the layout path in TypoScript
local/project_theme/Configuration/TypoScript/Extensions/SgVimeo/Constants.typoscript
```
plugin.tx_sgvimeo {
......@@ -100,7 +100,7 @@ plugin.tx_sgvimeo {
```
- Create a new layout file omitting the assets that you would like to change (for example, loading without CSS)
- Create a new layout file omitting the assets that you would like to change (for example, loading without CSS)
```
<div class="tx-sg-vimeo">
......@@ -110,14 +110,26 @@ plugin.tx_sgvimeo {
```
- Import the CSS or JavaScript source files in your respective asset pipeline and add them externally.
- Import the CSS or JavaScript source files in your respective asset pipeline and add them externally.
### Compiling CSS/JS assets with SGC
- Install the sgalinski/sgc-core library via composer
- Add the sg-vimeo extension paths in the .sgc-config.json
- Remove the loading of the compiled CSS/JS assets from Resources/Private/Templates/Vimeo/Index.html
- Add import the scss and js module file in the corresponding main.scss and main.js
- Initialize the javascript modules in your main.js: ```
new SgVideoLightbox();
SgVideo.initDefault();```
- Install the sgalinski/sgc-core library via composer
- Add the sg-vimeo extension paths in the .sgc-config.json
- Remove the loading of the compiled CSS/JS assets from Resources/Private/Templates/Vimeo/Index.html
- Add import the scss and js module file in the corresponding main.scss and main.js
- Initialize the javascript modules in your main.js: `
new SgVideoLightbox();
SgVideo.initDefault();`
### Compiling SASS only without SGC
#### Example:
`npm install -g sass`
`npx sass ./Resources/Public/Sass/Bootstrap5/main.scss ./Resources/Public/StyleSheets/Bootstrap5/main.min.css --style compressed --no-source-map`
### Using the Bootstrap 5 templates
If you want to use the Bootstrap 5 templates, you have to first install Bootstrap 5 in your theme to use its styles and JavaScript.
Afterwards simply set the `plugin.tx_project_theme.config.bootstrapVersion` TypoScript setup variable to 5.
<f:asset.css identifier="sgVideoCss" href="EXT:sg_vimeo/Resources/Public/StyleSheets/Bootstrap5/main.min.css" />
<f:asset.script identifier="sgVideoJs" src="EXT:sg_vimeo/Resources/Public/JavaScript/Dist/main.bundled.min.js" />
<f:render section="main"/>
{namespace vi=SGalinski\SgVimeo\ViewHelpers}
<f:layout name="Default" />
<f:section name="main">
<f:if condition="{showApiResult}">
{response -> f:debug()}
</f:if>
<f:variable name="feedCount" value="{response.items -> f:count()}" />
{vi:structuredVideoData(videoArray: '{response.items}', arrayType: 'vimeo')}
<f:if condition="{error}">
<f:then>
<p>{error}</p>
</f:then>
<f:else>
<f:comment><!--Set the layout to single if there is only one item.--></f:comment>
<div class="row">
<f:if condition="{feedCount} < 2">
<f:then>
<div class="col">
<f:render section="videoItem" arguments="{
feedItem: response.items.0,
titleChars: settings.maxTitleChars,
descChars: settings.maxDescriptionChars
}" />
</div>
</f:then>
<f:else>
<f:if condition="{feedCount} < 4 && {settings.layout} !== 'rows'">
<f:then>
<f:render section="list" arguments="{_all}" />
</f:then>
<f:else>
<f:switch expression="{settings.layout}">
<f:case value="playlist">
<f:render section="list-playlist" arguments="{_all}" />
</f:case>
<f:case value="rows">
<div class="col">
<ul class="list-group list-group-flush">
<f:render section="list" arguments="{_all}" />
</ul>
</div>
</f:case>
<f:defaultCase>
<f:render section="list" arguments="{_all}" />
</f:defaultCase>
</f:switch>
</f:else>
</f:if>
</f:else>
</f:if>
</div>
</f:else>
</f:if>
</f:section>
<f:section name="list">
<f:variable name="isRowsLayout"><f:if condition="{settings.layout} === 'rows'">1</f:if></f:variable>
<f:for each="{response.items}" as="feedItem" iteration="feedIterator">
<f:if condition="{isRowsLayout}">
<f:then>
<li class="list-group-item border-0 px-0 bg-transparent">
<f:render section="videoItem" arguments="{
feed: feed,
feedItem: feedItem,
titleChars: settings.maxTitleChars,
descChars: settings.maxDescriptionChars,
expand: 1,
feedIterator: feedIterator.index,
contentUid: contentUid,
isRowsLayout: 1
}" />
</li>
</f:then>
<f:else>
<div class="col-12 col-md-4">
<f:render section="videoItem" arguments="{
feed: feed,
feedItem: feedItem,
titleChars: settings.maxTitleChars,
descChars: settings.maxDescriptionChars,
expand: 1,
feedIterator: feedIterator.index,
contentUid: contentUid
}" />
</div>
</f:else>
</f:if>
</f:for>
</f:section>
<f:section name="list-playlist">
<div class="col-12 col-md-9">
<f:render section="videoItem" arguments="{
feedItem: response.items.0,
titleChars: settings.maxTitleChars,
descChars: settings.maxDescriptionChars
}" />
</div>
<div class="col-12 col-md-3 position-relative d-flex justify-content-center d-md-block">
<div class="row position-md-absolute overflow-auto w-100 h-100">
<f:for each="{response.items}" as="feedItem" iteration="feedIterator">
<f:if condition="!{feedIterator.isFirst}">
<div class="d-flex flex-column {f:if(condition: '{feedIterator.isLast}', then: 'pb-8')}">
<f:render section="videoItem" arguments="{
feedItem: feedItem,
titleChars: settings.maxTitleChars,
descChars: settings.maxDescriptionChars
}" />
</div>
</f:if>
</f:for>
</div>
</div>
</f:section>
<f:section name="videoItem">
<f:variable name="urlParameters">{f:if(condition: '{settings.urlParameters}', then: '{settings.urlParameters}', else: '{settings.globalUrlParameters}')}</f:variable>
<f:variable name="feedItemUrl">
<vi:urlWithQueryParameters url="{f:if(condition: '{feedItem.embedLink}', then: '{feedItem.embedLink}', else: '{feedItem.link}')}" parameters="{urlParameters}" />
</f:variable>
<f:variable name="feedItemId">{feedItem.videoId}</f:variable>
<f:variable name="hasText"><f:if condition="{settings.showTitle} || {settings.showDescription}">1</f:if></f:variable>
<f:variable name="hasDescription"><f:if condition="{settings.showDescription} && {feedItem.description}">1</f:if></f:variable>
<f:variable name="itemClasses"><f:if condition="{isRowsLayout}"><f:then>hstack flex-wrap gap-4 gap-sm-6 align-items-start</f:then><f:else>card shadow-small border-top h-100</f:else></f:if></f:variable>
<div class="sg-video__item {itemClasses}">
<a class="sg-video-item overflow-hidden text-light sg-video__link position-relative {f:if(condition: '!{hasText} || {isRowsLayout}', then: 'rounded')} {f:if(condition: '{isRowsLayout}', then: 'col-12 col-md-3 shadow', else: 'card-img-top')}" href="{feedItemUrl}" target="_blank" data-disable-lightbox="{settings.disableLightbox}" data-disable-lightbox-mobile="{settings.disableLightboxMobile}" data-additional-url-parameters="{urlParameters}" data-video-type="vimeo">
<div class="sg-video__svg position-absolute top-50p start-50p translate-middle z-1">
<span class="sg-video__svg-inner d-flex shadow text-bg-black bg-opacity-50 rounded-circle justify-content-center p-2">
<vi:renderSvg color="currentColor" name="solid-play" width="24" height="24"></vi:renderSvg>
</span>
</div>
<div class="overflow-hidden">
<f:if condition="{feedItem.thumbnail}">
<f:then>
<img class="sg-video__image object-fit-cover h-100 w-100" src="{feedItem.thumbnail}" alt="{feedItem.name}" loading="lazy" />
</f:then>
<f:else>
<f:if condition="{feedItem.pictures.base_link}">
<img class="sg-video__image object-fit-cover h-100 w-100" src="{feedItem.pictures.base_link}_{feedItem.width}x{feedItem.height}?r=pad" alt="{item.name}" width="{feedItem.width}" height="{feedItem.height}" loading="lazy" />
</f:if>
</f:else>
</f:if>
</div>
</a>
<f:if condition="{settings.showTitle} || {settings.showDescription}">
<div class="vstack justify-content-start {f:if(condition: '{isRowsLayout}', then: 'col-12 col-md-8 ps-4 ps-sm-0 pt-sm-4')}">
<div class="sg-video__bodytext">
<f:if condition="{settings.showTitle} && {feedItem.name}">
<div class="card-header {f:if(condition: '!{isRowsLayout} && !{hasDescription}', then: 'text-center')}">
<h3 class="card-title h4">
<f:if condition="{titleChars}">
<f:then>
<!-- This MUST be in one line, otherwise the crop function will count the whitespace -->
<f:format.crop maxCharacters="{titleChars}" respectWordBoundaries="1" append="..."><f:format.htmlentitiesDecode>{feedItem.name}</f:format.htmlentitiesDecode></f:format.crop>
</f:then>
<f:else>
<f:format.htmlentitiesDecode>{feedItem.name}</f:format.htmlentitiesDecode>
</f:else>
</f:if>
</h3>
</div>
</f:if>
<f:if condition="{settings.showDescription} && {feedItem.description}">
<div class="sg-video__description-collapse card-body {f:if(condition: '{isRowsLayout}', then: 'after-none mb-4 mb-sm-0')}">
<f:variable name="maxChars"><f:if condition="{descChars}"><f:then>{descChars}</f:then><f:else>100</f:else></f:if></f:variable>
<span><f:format.crop maxCharacters="{maxChars}" respectWordBoundaries="1" append="">{feedItem.description}</f:format.crop></span>
<span class="collapse" id="sg-video__description-{feedItemId}"><f:format.nl2br><vi:cropOpposite maxCharacters="{maxChars}" respectWordBoundaries="1">{feedItem.description}</vi:cropOpposite></f:format.nl2br></span>
<f:if condition="{settings.layout} == 'rows'">
<f:render section="readMoreButton" arguments="{feedItemId: feedItemId}" />
</f:if>
<f:if condition="{settings.showDescription} && {settings.layout} == 'default'">
<f:render section="readMoreButton" arguments="{feedItemId: feedItemId}" />
</f:if>
</div>
</f:if>
</div>
</div>
</f:if>
</div>
</f:section>
<f:section name="readMoreButton">
<a href="#" class="sg-video__read-more text-decoration-none text-lowercase" data-bs-toggle="collapse" data-bs-target="#sg-video__description-{feedItemId}" aria-expanded="false" aria-controls="sg-video__description-{feedItemId}">
<span class="sg-video__read-more-text d-inline-flex align-items-center">...{f:translate(key: 'frontend.readMore', extensionName: 'sg_vimeo')}&nbsp;<vi:renderSvg color="currentColor" name="solid-chevron-down" class="sg-video__read-more-arrow" width="12" height="12"></vi:renderSvg></span>
<span class="sg-video__read-less-text d-inline-flex align-items-center">{f:translate(key: 'frontend.readLess', extensionName: 'sg_vimeo')}&nbsp;<vi:renderSvg color="currentColor" name="solid-chevron-down" class="sg-video__read-more-arrow" width="12" height="12"></vi:renderSvg></span>
</a>
</f:section>
@import '../Modules/Bootstrap5/sg-video';
// Vars ---------------------------------------------------------------- //
$basicLightbox__background: rgba(0, 0, 0, 0.8) !default;
$basicLightbox__zIndex: 1020 !default;
$basicLightbox__duration: 0.4s !default;
$basicLightbox__timing: ease !default;
.sg-video__link {
&:hover {
.sg-video__svg-inner {
animation: pulse 1.4s linear infinite;
}
.sg-video__image {
transform: scale(1.1);
}
}
}
.sg-video__read-more-arrow {
transform: rotate(180deg);
}
.sg-video__read-more-text {
display: none;
}
.collapse:not(.show) + .sg-video__read-more {
.sg-video__read-less-text {
display: none;
}
.sg-video__read-more-text {
display: inline-flex;
}
.sg-video__read-more-arrow {
transform: none;
}
}
.sg-video__image {
transition: transform 0.3s ease;
}
.sg-video__svg {
svg {
transform: translateX(3px);
}
.no-lightbox & {
display: none !important;
}
}
.sg-video__svg-inner {
opacity: 0.5;
transition: opacity 0.3s ease, transform 0.3s ease;
}
// basicLightbox ------------------------------------------------------- //
.basicLightbox {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: $basicLightbox__background;
opacity: 0.01; // Start with .01 to avoid the repaint that happens from 0 to .01
transition: opacity $basicLightbox__duration $basicLightbox__timing;
z-index: $basicLightbox__zIndex;
will-change: opacity;
::after {
content: '';
position: absolute;
top: 1.8rem;
right: 1.8rem;
width: 2em;
height: 2em;
background-size: contain;
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20384%20512%22%20width%3D%2218%22%20height%3D%2218%22%20fill%3D%22white%22%20stroke%3D%22white%22%3E%3C!--!%20Font%20Awesome%20Free%206.5.1%20by%20%40fontawesome%20-%20https%3A%2F%2Ffontawesome.com%20License%20-%20https%3A%2F%2Ffontawesome.com%2Flicense%2Ffree%20(Icons%3A%20CC%20BY%204.0%2C%20Fonts%3A%20SIL%20OFL%201.1%2C%20Code%3A%20MIT%20License)%20Copyright%202023%20Fonticons%2C%20Inc.%20--%3E%3Cg%20id%3D%22svg-dff5ea2525636251b1cf60f8943ff15f%22%3E%3Cpath%20d%3D%22M342.6%20150.6c12.5-12.5%2012.5-32.8%200-45.3s-32.8-12.5-45.3%200L192%20210.7%2086.6%20105.4c-12.5-12.5-32.8-12.5-45.3%200s-12.5%2032.8%200%2045.3L146.7%20256%2041.4%20361.4c-12.5%2012.5-12.5%2032.8%200%2045.3s32.8%2012.5%2045.3%200L192%20301.3%20297.4%20406.6c12.5%2012.5%2032.8%2012.5%2045.3%200s12.5-32.8%200-45.3L237.3%20256%20342.6%20150.6z%22%20fill%3D%22white%22%20stroke%3D%22white%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
cursor: pointer;
}
&--visible {
opacity: 1;
pointer-events: auto;
}
&__placeholder {
max-width: 100%;
transform: scale(0.9);
transition: transform $basicLightbox__duration $basicLightbox__timing;
z-index: 1;
will-change: transform;
> img:first-child:last-child,
> video:first-child:last-child,
> iframe:first-child:last-child {
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
> video:first-child:last-child,
> iframe:first-child:last-child {
pointer-events: auto;
}
> img:first-child:last-child,
> video:first-child:last-child {
width: auto;
height: auto;
}
}
&--img &__placeholder,
&--video &__placeholder,
&--iframe &__placeholder {
width: 100%;
height: 100%;
pointer-events: none;
}
&--visible &__placeholder {
transform: scale(1);
}
}
iframe.sg-video-iframe {
width: 100%;
height: auto;
max-width: 900px;
aspect-ratio: 16/9;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
opacity: 0.75;
}
100% {
transform: scale(1.1);
opacity: 1;
}
}
.sg-video__link:hover .sg-video__svg-inner{animation:pulse 1.4s linear infinite}.sg-video__link:hover .sg-video__image{transform:scale(1.1)}.sg-video__read-more-arrow{transform:rotate(180deg)}.sg-video__read-more-text{display:none}.collapse:not(.show)+.sg-video__read-more .sg-video__read-less-text{display:none}.collapse:not(.show)+.sg-video__read-more .sg-video__read-more-text{display:inline-flex}.collapse:not(.show)+.sg-video__read-more .sg-video__read-more-arrow{transform:none}.sg-video__image{transition:transform .3s ease}.sg-video__svg svg{transform:translateX(3px)}.no-lightbox .sg-video__svg{display:none !important}.sg-video__svg-inner{opacity:.5;transition:opacity .3s ease,transform .3s ease}.basicLightbox{position:fixed;display:flex;justify-content:center;align-items:center;top:0;left:0;width:100%;height:100vh;background:rgba(0,0,0,.8);opacity:.01;transition:opacity .4s ease;z-index:1020;will-change:opacity}.basicLightbox ::after{content:"";position:absolute;top:1.8rem;right:1.8rem;width:2em;height:2em;background-size:contain;background-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20384%20512%22%20width%3D%2218%22%20height%3D%2218%22%20fill%3D%22white%22%20stroke%3D%22white%22%3E%3C!--!%20Font%20Awesome%20Free%206.5.1%20by%20%40fontawesome%20-%20https%3A%2F%2Ffontawesome.com%20License%20-%20https%3A%2F%2Ffontawesome.com%2Flicense%2Ffree%20(Icons%3A%20CC%20BY%204.0%2C%20Fonts%3A%20SIL%20OFL%201.1%2C%20Code%3A%20MIT%20License)%20Copyright%202023%20Fonticons%2C%20Inc.%20--%3E%3Cg%20id%3D%22svg-dff5ea2525636251b1cf60f8943ff15f%22%3E%3Cpath%20d%3D%22M342.6%20150.6c12.5-12.5%2012.5-32.8%200-45.3s-32.8-12.5-45.3%200L192%20210.7%2086.6%20105.4c-12.5-12.5-32.8-12.5-45.3%200s-12.5%2032.8%200%2045.3L146.7%20256%2041.4%20361.4c-12.5%2012.5-12.5%2032.8%200%2045.3s32.8%2012.5%2045.3%200L192%20301.3%20297.4%20406.6c12.5%2012.5%2032.8%2012.5%2045.3%200s12.5-32.8%200-45.3L237.3%20256%20342.6%20150.6z%22%20fill%3D%22white%22%20stroke%3D%22white%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E");background-repeat:no-repeat;cursor:pointer}.basicLightbox--visible{opacity:1;pointer-events:auto}.basicLightbox__placeholder{max-width:100%;transform:scale(0.9);transition:transform .4s ease;z-index:1;will-change:transform}.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child,.basicLightbox__placeholder>iframe:first-child:last-child{display:block;position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.basicLightbox__placeholder>video:first-child:last-child,.basicLightbox__placeholder>iframe:first-child:last-child{pointer-events:auto}.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{width:auto;height:auto}.basicLightbox--img .basicLightbox__placeholder,.basicLightbox--video .basicLightbox__placeholder,.basicLightbox--iframe .basicLightbox__placeholder{width:100%;height:100%;pointer-events:none}.basicLightbox--visible .basicLightbox__placeholder{transform:scale(1)}iframe.sg-video-iframe{width:100%;height:auto;max-width:900px;aspect-ratio:16/9}@keyframes pulse{0%{transform:scale(1);opacity:1}50%{opacity:.75}100%{transform:scale(1.1);opacity:1}}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment