Compare commits

..

1 commit

Author SHA1 Message Date
ae3cd2e856 prepare wall for bdk 2023-11-08 18:18:42 +01:00
12 changed files with 98 additions and 361 deletions

View file

@ -1,10 +1,9 @@
# Mastowall 1.1
Forked from [https://github.com/rstockm/mastowall](https://github.com/rstockm/mastowall)
Mastowall is a social wall application that displays posts from the [Mastodon](https://joinmastodon.org/) social network based on specified hashtags. It was written entirely by [ChatGPT4](https://openai.com/product/gpt-4), guided only by text prompts.
<img width="1348" alt="image" src="https://git.verdigado.com/NB-Public/mastowall/raw/branch/main/screenshot.jpg">
<img width="1348" alt="image" src="https://github.com/rstockm/mastowall/assets/3195116/7060536e-4847-4e38-801e-3c0312b8b194">
Try it live: [Mastowall for BDK23](https://tretkowski.de/mastowall/?hashtags=bdk23,netzbegruenung&server=https://gruene.social)
@ -38,8 +37,6 @@ JSON config file:
- **Including Replies:** By default, replies are excluded from the wall. However, this behavior can be changed by setting includeReplies to true in the `config.json` file.
- **Configurable Overlay:** By default only the MastoWall is shown. For use with large displays eg during trade shows, conferences, booths, you can enable the Carousel with enlarged display of the 10 most recent posts. Just add `nbstand=1` to the argument in the URL.
## Technology Stack
Mastowall is built using the following technologies:
@ -66,7 +63,7 @@ Mastowall is built using the following technologies:
## Sharing via URL
Mastowall supports URL parameters to easily share specific hashtag configurations and the Mastodon server. Simply append the desired hashtags and the server URL to the URL following this format: `?hashtags=hashtag1,hashtag2,hashtag3&server=serverUrl&nbstand=0`
Mastowall supports URL parameters to easily share specific hashtag configurations and the Mastodon server. Simply append the desired hashtags and the server URL to the URL following this format: `?hashtags=hashtag1,hashtag2,hashtag3&server=serverUrl`
Enjoy using Mastowall!

View file

@ -1,16 +0,0 @@
## Additions made by SSB for BDK23
# added carousel for posts
## Important
install color emojis:
sudo apt-get install fonts-noto-color-emoji
install xdotool to move the mouse to bottom right (does not work with Bookworm/Wayland)
sudo apt-get install xdotool -y
run in kiosk mode:
chromium-browser --kiosk index.html
or use the remote URL instead of index.html

View file

@ -1,15 +1,6 @@
{
"navbarBrandText": "Netzbegrünung Mastowall - gruene.social",
"navbarBrandText": "Netzbegrünung Mastowall",
"defaultServerUrl": "https://gruene.social",
"navbarColor": "#008939",
"duration": 10,
"refreshDuration": 30,
"maxAge": 604800,
"extraCards": [
"<img src='sharepics/Slide1.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide2.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide3.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide4.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>"
],
"includeReplies": true
}

View file

@ -4,20 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Netzbegrünung Mastowall</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="bootstrap.min.css">
<link rel="stylesheet" href="styles.css">
<link rel="apple-touch-icon" href="mastowall-favicon.png">
<link rel="icon" href="mastowall-favicon.png" type="image/x-icon">
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.3/purify.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<nav class="navbar navbar-light">
@ -38,11 +28,11 @@
</div>
<div class="form-group">
<label for="hashtag2">Hashtag 2:</label>
<input type="text" class="form-control" id="hashtag2" placeholder="Enter a hashtag" value="bdk24">
<input type="text" class="form-control" id="hashtag2" placeholder="Enter a hashtag" value="bdk23">
</div>
<div class="form-group">
<label for="hashtag3">Hashtag 3:</label>
<input type="text" class="form-control" id="hashtag3" placeholder="Enter a hashtag" value="wasjetztzählt">
<input type="text" class="form-control" id="hashtag3" placeholder="Enter a hashtag">
</div>
<div class="form-group">
<label for="serverUrl">Server:</label>
@ -60,11 +50,6 @@
<div class="row masonry-grid" id="wall"></div>
</div>
<div id="popover" class="popover">
<div id="myCarousel" class="carousel slide" data-mdb-ride="carousel" data-mdb-pause="false">
</div>
</div>
<footer class="footer text-center py-4 mt-5">
<div class="container">
<a href="https://netzbegruenung.de/" target="_blank">Netzbegrünung</a>
@ -72,5 +57,10 @@
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="masonry.pkgd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.3/purify.min.js"></script>
<script src="script.js"></script>
</body>
</html>

View file

@ -1,6 +0,0 @@
[Desktop Entry]
Type=Application
Name=MastoWall Autostart
Comment=Starten der MastoWall zur BDK23
NoDisplay=false
Exec=sh -c 'cd /home/ssb/Documents/mastowall && xdotool mousemove 1920 1080 && sleep 5 && chromium-browser --kiosk index.html'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

261
script.js
View file

@ -1,12 +1,13 @@
// The existingPosts array is used to track already displayed posts
let existingPosts = [];
var cardIterator = 0;
// getUrlParameter helps to fetch URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(location.search);
console.log(results)
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
@ -17,46 +18,23 @@ const secondsAgo = date => Math.floor((new Date() - date) / 1000);
const timeAgo = function(seconds) {
// An array of intervals for years, months, days, hours, and minutes.
const intervals = [
{ limit: 31536000, singular: 'Jahre', plural: 'Jahren' },
{ limit: 2592000, singular: 'Monat', plural: 'Monaten' },
{ limit: 86400, singular: 'Tag', plural: 'Tagen' },
{ limit: 3600, singular: 'Stunde', plural: 'Stunden' },
{ limit: 60, singular: 'Minute', plural: 'Minuten' }
{ limit: 31536000, text: 'Jahren' },
{ limit: 2592000, text: 'Monaten' },
{ limit: 86400, text: 'Tagen' },
{ limit: 3600, text: 'Stunden' },
{ limit: 60, text: 'Minuten' }
];
// Loop through the intervals to find which one is the best fit.
for (let interval of intervals) {
if (seconds >= interval.limit) {
let amount = Math.floor(seconds / interval.limit);
let text;
if (amount !== 1) {
text = interval.plural;
} else {
text = interval.singular;
}
return `Vor ${amount} ${text}`;
return "Vor " + Math.floor(seconds / interval.limit) + ` ${interval.text}`;
}
}
let text = "Sekunde";
let amount = Math.floor(seconds);
if (amount !== 1) {
text += "n";
}
return `Vor ${amount} ${text}`;
return "Vor " + Math.floor(seconds) + " Sekunden";
};
let includeReplies;
// max post age in seconds
let maxAge;
// below times are in milliseconds
// duration for slide animations
let duration;
// refresh rate
let refresh;
// extra cards text
let extraCards;
// toggle Carousel
let withCarousel=false;
// fetchConfig fetches the configuration from the config.json file
const fetchConfig = async function() {
@ -65,26 +43,9 @@ const fetchConfig = async function() {
$('#navbar-brand').text(config.navbarBrandText);
$('.navbar').css('background-color', config.navbarColor);
includeReplies = config.includeReplies;
maxAge = config.maxAge;
duration = config.duration * 1000;
refresh = config.refreshDuration * 1000;
extraCards = config.extraCards;
return config.defaultServerUrl;
} catch (error) {
console.log("Error loading config.json:", error);
$('#navbar-brand').text("Netzbegrünung Mastowall - gruene.social");
$('.navbar').css('background-color', "#008939");
includeReplies = true;
maxAge = 60 * 60 * 24 * 7;
duration = 10000;
refresh = 30000;
extraCards = [
"<img src='sharepics/Slide1.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide2.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide3.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide4.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>"
];
return "https://gruene.social";
console.error("Error loading config.json:", error);
}
}
@ -100,44 +61,31 @@ const fetchPosts = async function(serverUrl, hashtag) {
// updateTimesOnPage updates the time information displayed for each post
const updateTimesOnPage = function() {
$('.card-text a.time').each(function() {
const timeValue = $(this).attr('data-time');
if (timeValue === '') return;
const date = new Date(timeValue);
$('.card-text a').each(function() {
const date = new Date($(this).attr('data-time'));
const newTimeAgo = timeAgo(secondsAgo(date));
$(this).text(newTimeAgo);
});
};
// replace certain emojies in some text with images
const replaceEmojies = (text, emojis) => {
emojis.forEach(emoji => {
text = text.replaceAll(`:${emoji.shortcode}:`, `<img class="emoji" src="${emoji.static_url}">`);
});
return text;
};
// displayPost creates and displays a post
const displayPost = function(post) {
if (existingPosts.includes(post.id) || (!includeReplies && post.in_reply_to_id !== null)) return 0;
if (existingPosts.includes(post.id) || (!includeReplies && post.in_reply_to_id !== null)) return;
existingPosts.push(post.id);
let cardHTML = `
<div class="col-sm-3">
<div class="col-sm-2">
<div class="card m-2 p-2">
<div class="d-flex align-items-center mb-2">
<img src="${post.account.avatar}" class="avatar-img rounded-circle mr-2">
<p class="m-0">${replaceEmojies(DOMPurify.sanitize(post.account.display_name), post.account.emojis)} <span class="user-name">@${DOMPurify.sanitize(post.account.acct)}</span></p>
<p class="m-0">${DOMPurify.sanitize(post.account.display_name)}</p>
</div>
${post.media_attachments[0] ?
(post.media_attachments[0].url.endsWith('.mp4') ?
`<video src="${post.media_attachments[0].url}" controls autoplay muted loop></video>` :
`<img src="${post.media_attachments[0].url}" class="card-img-top mb-2">`) :
''}
<p class="card-text">${replaceEmojies(DOMPurify.sanitize(post.content), post.emojis)}</p>
${post.spoiler_text ? `<p class="card-text text-muted spoiler">${DOMPurify.sanitize(post.spoiler_text)}</p>` : ''}
<p class="card-text text-right"><small class="text-muted"><a class="time" href="${post.url}" target="_blank" data-time="${post.created_at}">${timeAgo(secondsAgo(new Date(post.created_at)))}</a></small></p>
<div class="row">
${post.media_attachments[0] ? `<div class="col-md-6"><p class="card-text">${DOMPurify.sanitize(post.content)}</p></div>` : `<div class="col-md-12"><p class="card-text">${DOMPurify.sanitize(post.content)}</p></div>`}
${post.media_attachments[0] ? `<div class="col-mr-6"><img src="${post.media_attachments[0].url}" class="card-img-bottom"> </div>` : ''}
</div>
<p class="card-text text-right"><small class="text-muted"><a href="${post.url}" target="_blank" data-time="${post.created_at}">${timeAgo(secondsAgo(new Date(post.created_at)))}</a></small></p>
</div>
</div>
`;
@ -145,124 +93,16 @@ const displayPost = function(post) {
let $card = $(cardHTML);
$('#wall').prepend($card);
$('.masonry-grid').masonry('prepended', $card);
return 1;
};
const processPosts = function(posts) {
posts = posts.filter((post) => {
return secondsAgo(new Date(post.created_at)) < maxAge && post.content.indexOf("nitter.") === -1 && post.spoiler_text === ""
});
return posts;
};
// updateWall displays all posts
const updateWall = function(posts) {
if (!posts || posts.length === 0) return;
posts = processPosts(posts);
posts.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
let ret = 0
posts.forEach(post => ret += displayPost(post));
$('.masonry-grid').masonry('layout');
return ret;
posts.forEach(post => displayPost(post));
};
// updateCarousel
const updateCarousel = function(slides, posts) {
if (!posts || posts.length === 0) return;
posts = processPosts(posts);
posts.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
// remove slides in carousel
slides.innerHTML = "";
var newHTML = ` <!-- No Indicators -->`
newHTML += `<!-- the slides -->
<div class="carousel-inner">
`;
let existingCards = [];
for( let i = 0; i < posts.length; i++ ) {
let post = posts[i];
if (existingCards.includes(post.id) || (!includeReplies && post.in_reply_to_id !== null)) continue;
existingCards.push(post.id);
/*console.log( post.content )*/
if ( i == 0 ) {
newHTML += `<div class="carousel-item active" data-interval="${duration}" data-pause="false">`;
}
else {
newHTML += `<div class="carousel-item" data-interval="${duration}" data-pause="false">`;
}
const postContent = replaceEmojies(DOMPurify.sanitize(post.content), post.emojis);
newHTML += `
<div class="card-big">
<div class="d-flex align-items-center mb-4">
<img src="${post.account.avatar}" class="avatar-img-big rounded-circle mr-4">
<p class="avatar-name">${replaceEmojies(DOMPurify.sanitize(post.account.display_name), post.account.emojis)} <span class="user-name">@${DOMPurify.sanitize(post.account.acct)}</span></p>
</div>
<hr>
<div class="row align-items-center vertical-align-center">
<div class="${post.media_attachments[0] ? `col-md-6` : `col-md-12`}"><div class="card-text" ${strip(postContent).length > 700 ? `style="font-size: ${strip(postContent).length > 1000 ? `0.5`:`0.9`}em;"`: ``}>${postContent}</div></div>
${post.media_attachments[0] ?
(post.media_attachments[0].url.endsWith('.mp4') ?
`<div class="col-md-6"><video src="${post.media_attachments[0].url}" controls autoplay muted loop class="card-img-bottom" align="center"></video></div>` :
`<div class="col-md-6"><img src="${post.media_attachments[0].url}" class="card-img-bottom" align="center"> </div>`) :
''}
</div>
<hr>
<p class="card-text text-right">
<small class="text-muted"><a class="time" href="${post.url}" target="_blank" data-time="${post.created_at}">${timeAgo(secondsAgo(new Date(post.created_at)))}</a>
${post.favourites_count ? `, <b>${post.favourites_count}</b> mal favorisiert` : '' }
${post.replies_count ? `, <b>${post.replies_count}</b> mal kommentiert` : '' }
${post.reblogs_count ? `, <b>${post.reblogs_count}</b> mal geteilt` : '' }
</small>
</p>
</div>
`;
newHTML += '</div>';
}
for( let i = 0; i < extraCards.length; i++ ) {
newHTML += `<div class="carousel-item" data-interval="${duration * 2}" data-pause="false">
<div className="card-big">
<div class="d-flex align-items-center mb-4">
${extraCards[i]}
</div>
</div>
</div>`;
}
newHTML += '</div>'
document.getElementById("myCarousel").innerHTML = newHTML;
};
const showCarousel = function() {
// show popover
document.getElementById('popover').style.opacity = '1';
document.getElementById('popover').style.display = 'block';
// Activate Carousel
$('#myCarousel').carousel("cycle");
}
const strip = function(html) {
let doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || "";
}
const hideCarousel = function() {
// hide popover
document.getElementById('popover').style.display = 'none';
// Activate Carousel
}
// hashtagsString returns a single string based on the given array of hashtags
const hashtagsString = function(hashtagsArray) {
return `${hashtagsArray.map(hashtag => `#${hashtag}`).join(' ')}`;
@ -285,9 +125,6 @@ const handleHashtagDisplayClick = function(serverUrl) {
$('#zero-state').removeClass('d-none');
const currentHashtags = getUrlParameter('hashtags').split(',');
if ( currentHashtags = null ) {
currentHasttags = "netzbegruenung,bdk24,wasjetztzählt".split(',')
}
for (let i = 0; i < currentHashtags.length; i++) {
$(`#hashtag${i+1}`).val(currentHashtags[i]);
@ -322,6 +159,11 @@ const handleHashtagFormSubmit = function(e, hashtagsArray) {
window.location.href = newUrl;
};
// make singe card bigger by switching class
const toggleSize = function(child,i) {
console.log( child )
}
// On document ready, the script configures Masonry, handles events, fetches and displays posts
$(document).ready(async function() {
const defaultServerUrl = await fetchConfig();
@ -333,50 +175,38 @@ $(document).ready(async function() {
setInterval(function() {
$('.masonry-grid').masonry('layout');
}, refresh);
}, 1000);
let hashtags = getUrlParameter('hashtags');
if ( hashtags == '' ) {
hashtags = "netzbegruenung,bdk24,wasjetztzählt";
}
const hashtags = getUrlParameter('hashtags');
const hashtagsArray = hashtags ? hashtags.split(',') : [];
const serverUrl = getUrlParameter('server') || defaultServerUrl;
const enableCarousel = getUrlParameter('nbstand' );
if ( enableCarousel == '1' )
withCarousel = true;
console.log("show carousel: "+withCarousel);
$('#hashtag-display').on('click', function() {
handleHashtagDisplayClick(serverUrl);
});
const slides = $('#myCarousel');
const popover = $('#popover');
if (hashtagsArray.length > 0 && hashtagsArray[0] !== '') {
let allPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag)));
const allPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag)));
updateWall(allPosts.flat());
setTimeout(function() {
$('.masonry-grid').masonry('layout');
}, 2000);
if ( withCarousel) {
updateCarousel(slides, allPosts.flat());
setTimeout(async function() { showCarousel(); }, duration);
}
else setTimeout(async function() { hideCarousel(); }, duration);
setInterval(async function() {
const newPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag)));
let updated = updateWall(newPosts.flat());
if ( withCarousel && updated > 0 ) {
updateCarousel(slides, newPosts.flat());
}
}, refresh);
updateWall(newPosts.flat());
}, 10000);
setInterval(function() {
var mason = $('.masonry-grid').data('masonry')
//console.log( cardIterator )
mason.element.children[cardIterator].classList.value = 'col-sm-2'
//mason.element.children[cardIterator].children[0].classList.toggle( '' )
//console.log( mason.element.children[cardIterator].classList )
cardIterator++
if ( cardIterator >= mason.element.childNodes.length )
cardIterator = 0
mason.element.children[cardIterator].classList.value = 'col-big'
}, 1000);
} else {
$('#zero-state').removeClass('d-none');
$('#popover').removeClass('popover');
$('#app-content').addClass('d-none');
}
@ -387,7 +217,6 @@ $(document).ready(async function() {
handleHashtagFormSubmit(e, hashtagsArray);
});
updateTimesOnPage();
setInterval(updateTimesOnPage, 60000);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View file

@ -1,18 +1,14 @@
@charset "utf-8";
/* Add some custom CSS for the cards */
.card {
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.2);
background-color: rgb(240,255,240);
}
/* Position the avatar and username in the top left of the card */
.card-avatar {
height: 50px;
width: 50px;
height: 80px;
width: 80px;
border-radius: 50%;
position: absolute;
top: 10px;
@ -48,16 +44,11 @@
.card-text {
margin-bottom: 1px !important;
}
.carousel .card-text {
font-size: 1.6em;
overflow: hidden;
max-height: 50vh;
text-align: left;
}
.card {
font-size: 0.9em; /* adjust this value to get the desired text size */
font-size: 1.5em; /* adjust this value to get the desired text size */
}
/* Remove indent of URLs */
@ -72,8 +63,8 @@
/* Custom navbar styles */
.navbar {
min-height: 50px; /* reduce the height of the navbar */
background-color: rgb(0, 137, 57);
height: 50px; /* reduce the height of the navbar */
background-color: rgb(227, 6, 19);
margin-bottom: 10px !important;
top: -10px !important;
padding-top: 14px !important;
@ -82,13 +73,13 @@
.navbar-brand {
color: rgba(255, 255, 255, 0.8) !important; /* change the text color */
margin: 0 auto; /* center the brand name */
font-size: 1.8em;
font-size: 0.9em;
}
.navbar-info {
color: rgba(255, 255, 255, 0.8) !important; /* change the text color */
margin: 0 auto; /* center the brand name */
font-size: 2.4em;
font-size: 1.2em;
display: block !important;
}
@ -134,28 +125,12 @@ body {
height: 50px;
}
.avatar-img-big {
width: 100px;
height: 100px;
}
.avatar-name {
font-size:2em;
font-weight:600;
padding-top:0.3em
}
.user-name {
font-weight: normal;
opacity: 0.7;
}
.container {
max-width: 2000px !important;
}
.footer {
background-color: rgb(0, 137, 57);
background-color: rgb(200, 200, 200);
color: #f2f2f2;
position: fixed;
left: 0;
@ -166,65 +141,42 @@ body {
font-size: 0.9em;
}
.popover {
position: fixed;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
max-height: 80%;
max-width: 80%;
background-color: rgba(255, 255, 255, .95);
background-clip: padding-box;
border: 0px solid rgba(0, 137, 57, .5);
box-shadow:0 .5rem 1rem rgba(0, 0, 0, .30); !important
border-radius:.8rem;
opacity: 0;
transition: opacity 0.5s linear;
.col-sm-2 {
/*-ms-flex: 0 0 99%;*/
/*flex: 0 0 99%;*/
/*width: 99%;*/
/*max-width: 99%;*/
/*height: 80%;*/
/*max-height: 80%;*/
position: absolute;
display: block
top: 0;
left: 0;
width: 0;
height: 0;
visibility: hidden
}
.card-big {
font-size: 1.8em;
font-weight: 400;
margin-top: 20px !important;
margin-left: 40px !important;
margin-right: 40px !important;
margin-bottom: 20px !important;
.col-big {
/*-ms-flex: 0 0 99%;*/
/*flex: 0 0 99%;*/
/*width: 99%;*/
/*max-width: 99%;*/
/*height: 80%;*/
/*max-height: 80%;*/
/*z-index: 2;*/
display: block;
position: absolute;
top: 10;
left: 10;
width: 100%;
height: 400;
transition: none
}
.card-img-bottom {
max-width: 600px;
max-height: 500px;
width: auto;
text-align: center;
aspect-ratio: auto !important;
max-height: 400px;
border-top-left-radius: calc(.25rem - 1px);
border-top-right-radius:calc(.25rem - 1px);
margin-top: 0px !important;
margin-bottom: 10px !important;
align-items: center;
float: right;
}
.text-muted {
color:#6c757d !important;
font-size: 1em;
text-align: right;
}
.footer {
font-size: 2em;
}
.footer .text-muted {
color: rgba(255, 255, 255, 0.8) !important;
font-size: 0.8em;
}
.footer a {
color: rgba(255, 255, 255, 0.8) !important;
}
.emoji {
height: 1em;
border-top-right-radius:calc(.25rem - 1px)
}