feat: share link page (#304)

* initial commit

* improving holocard and footer

* feat: better handling mobile

* fix: lint

* video pop up player

* fix : test

* not found page

* feat: improving video player UI

* fixing some footer links and player UI

* fix rewrites

* backdrop and some final fixes

* fixing share tests
This commit is contained in:
Erick
2023-01-12 14:57:25 -03:00
committed by GitHub
parent fbfb60320a
commit 2ccd6a212a
10 changed files with 618 additions and 228 deletions

View File

@ -13,7 +13,7 @@
"__/**"
],
"rewrites": [
{ "source": "/share/**", "function": "shareImage" },
{ "source": "/share/**", "function": "shareVideo" },
{
"source": "**",
"destination": "/index.html"
@ -63,7 +63,7 @@
"__/**"
],
"rewrites": [
{ "source": "/share/**", "function": "shareImage" },
{ "source": "/share/**", "function": "shareVideo" },
{
"source": "**",
"destination": "/index.html"

View File

@ -3,6 +3,7 @@ export const UPLOAD_PATH = 'uploads';
export const SHARE_PATH = 'share';
export const ALLOWED_HOSTS = [
'http://localhost:5001',
'http://localhost:5000',
'https://io-photobooth-dev.web.app',
'https://io-photo-booth.web.app',
'https://photobooth.flutter.dev',

View File

@ -1,4 +1,4 @@
import * as admin from 'firebase-admin';
admin.initializeApp();
export { shareImage } from './share';
export { shareVideo } from './share';
export { convert } from './convert';

View File

@ -24,7 +24,7 @@ describe('Share API', () => {
path: '',
protocol: 'http',
get(_: string) {
return 'http://localhost:5001';
return 'localhost:5001';
},
} as functions.https.Request;
@ -48,7 +48,7 @@ describe('Share API', () => {
test('Valid file name returns 200 and html', async () => {
const req = Object.assign({}, baseReq, {
path: 'test.png',
path: 'test.mp4',
});
const res = await getShareResponse(req);
expect(res.status).toEqual(200);

View File

@ -5,37 +5,50 @@ import * as querystring from 'querystring';
import mustache from 'mustache';
import { UPLOAD_PATH, ALLOWED_HOSTS } from '../config';
import footerTmpl from './templates/footer';
import notFoundTmpl from './templates/notfound';
import shareTmpl from './templates/share';
import footerTmpl from './templates/footer';
import stylesTmpl from './templates/styles';
import scriptsTmpl from './templates/scripts';
import gaTmpl from './templates/ga';
const VALID_IMAGE_EXT = [ '.png', '.jpeg', '.jpg' ];
const VALID_VIDEO_EXT = [ '.mp4' ];
const BaseHTMLContext: Record<string, string | Record<string, string>> = {
appUrl: '',
shareUrl: '',
shareImageUrl: '',
shareVideoUrl: '',
assetUrls: {
favicon: bucketPathForFile('public/favicon.png'),
bg: bucketPathForFile('public/background.jpg'),
bgMobile: bucketPathForFile('public/background-mobile.jpg'),
notFoundPhoto: bucketPathForFile('public/404-photo.png'),
fixedPhotosLeft: bucketPathForFile('public/table-photos-left.png'),
fixedPhotosRight: bucketPathForFile('public/table-photos-right.png'),
bg: bucketPathForFile('public/background.png'),
playArrowIcon: bucketPathForFile('public/play-arrow.png'),
shareIcon: bucketPathForFile('public/share.png'),
flutterForwardLogo: bucketPathForFile('public/flutter-forward-logo.png'),
videoFrame: bucketPathForFile('public/video-frame.png'),
flutterIcon: bucketPathForFile('public/flutter-icon.png'),
firebaseIcon: bucketPathForFile('public/firebase-icon.png'),
tensorflowIcon: bucketPathForFile('public/tensorflow-icon.png'),
holocard: bucketPathForFile('public/holocard.png'),
notFoundBg: bucketPathForFile('public/not-found-bg.png'),
notFoundMobileBg: bucketPathForFile('public/not-found-mobile-bg.png'),
playerPlay: bucketPathForFile('public/player-play.png'),
playerVolume: bucketPathForFile('public/player-volume.png'),
playerFullscreen: bucketPathForFile('public/player-fullscreen.png'),
close: bucketPathForFile('public/close.png'),
},
meta: {
title: 'Google I/O Photo Booth',
desc: (
'Take a photo in the I/O Photo Booth with your favorite Google Developer Mascots! ' +
'Built with Flutter & Firebase for Google I/O 2021.'
),
title: 'Flutter Forward Holobooth',
desc: [
'Jump into a new reality with the Flutter Holobooth! ',
'Bring Dash and Sparky to life in this open source demo ',
'built with Flutter, Firebase, Media Pipe & Tensorflow',
].join(''),
},
footer: footerTmpl,
ga: gaTmpl,
styles: '',
scripts: '',
footer: '',
};
@ -62,35 +75,37 @@ function renderTemplate(
tmpl: string, context: Record<string, string | Record<string, string>>
): string {
context.styles = mustache.render(stylesTmpl, context);
context.scripts = mustache.render(scriptsTmpl, context);
context.footer = mustache.render(footerTmpl, context);
return mustache.render(tmpl, context);
}
/**
* Render the 404 html page
* @param {string} imageFileName - filename of storage image
* @param {string} videoFileName - filename of storage video
* @param {string} baseUrl - http base fully qualified URL
* @return {string} HTML string
*/
function renderNotFoundPage(imageFileName: string, baseUrl: string): string {
function renderNotFoundPage(videoFileName: string, baseUrl: string): string {
const context = Object.assign({}, BaseHTMLContext, {
appUrl: baseUrl,
shareUrl: `${baseUrl}/share/${imageFileName}`,
shareImageUrl: bucketPathForFile(`${UPLOAD_PATH}/${imageFileName}`),
shareUrl: `${baseUrl}/share/${videoFileName}`,
shareVideoUrl: bucketPathForFile(`${UPLOAD_PATH}/${videoFileName}`),
});
return renderTemplate(notFoundTmpl, context);
}
/**
* Populate and return the share page HTML for given path
* @param {string} imageFileName - filename of storage image
* @param {string} videoFileName - filename of storage video
* @param {string} baseUrl - http base fully qualified URL
* @return {string} HTML string
*/
function renderSharePage(imageFileName: string, baseUrl: string): string {
function renderSharePage(videoFileName: string, baseUrl: string): string {
const context = Object.assign({}, BaseHTMLContext, {
appUrl: baseUrl,
shareUrl: `${baseUrl}/share/${imageFileName}`,
shareImageUrl: bucketPathForFile(`${UPLOAD_PATH}/${imageFileName}`),
shareUrl: `${baseUrl}/share/${videoFileName}`,
shareVideoUrl: bucketPathForFile(`${UPLOAD_PATH}/${videoFileName}`),
});
return renderTemplate(shareTmpl, context);
}
@ -106,33 +121,33 @@ export async function getShareResponse(
try {
const host = req.get('host') ?? '';
const baseUrl = `${req.protocol}://${host}`;
const { ext, base: imageFileName } = path.parse(req.path);
const { ext, base: videoFileName } = path.parse(req.path);
if (!ALLOWED_HOSTS.includes(host) || !VALID_IMAGE_EXT.includes(ext)) {
functions.logger.log('Bad host or image ext', { host, baseUrl, ext, imageFileName });
if (!ALLOWED_HOSTS.includes(baseUrl) || !VALID_VIDEO_EXT.includes(ext)) {
functions.logger.log('Bad host or video ext', { host, baseUrl, ext, videoFileName });
return {
status: 404,
send: renderNotFoundPage(imageFileName, baseUrl),
send: renderNotFoundPage(videoFileName, baseUrl),
};
}
const imageBlobPath = `${UPLOAD_PATH}/${imageFileName}`;
const imageExists = await admin.storage().bucket().file(imageBlobPath).exists();
const videoBlobPath = `${UPLOAD_PATH}/${videoFileName}`;
const videoExists = await admin.storage().bucket().file(videoBlobPath).exists();
if (Array.isArray(imageExists) && imageExists[0]) {
if (Array.isArray(videoExists) && videoExists[0]) {
return {
status: 200,
send: renderSharePage(imageFileName, baseUrl),
send: renderSharePage(videoFileName, baseUrl),
};
}
functions.logger.log('Image does not exist', { imageBlobPath });
functions.logger.log('Video does not exist', { videoBlobPath });
// NOTE 200 status so that default share meta tags work,
// where twitter does not show meta tags on a 404 status
return {
status: 200,
send: renderNotFoundPage(imageFileName, baseUrl),
send: renderNotFoundPage(videoFileName, baseUrl),
};
} catch (error) {
functions.logger.error(error);
@ -147,7 +162,7 @@ export async function getShareResponse(
/**
* Public sharing function
*/
export const shareImage = functions.https.onRequest(async (req, res) => {
export const shareVideo = functions.https.onRequest(async (req, res) => {
const { status, send } = await getShareResponse(req);
res.set('Access-Control-Allow-Origin', '*');
res.status(status).send(send);

View File

@ -1,19 +1,33 @@
export default `
<footer>
<div class="left">
<span>Made with
<a href="https://flutter.dev">Flutter</a> &amp;
<a href="https://firebase.google.com">Firebase</a>
</ul>
</div>
<div class="right">
<ul>
<li><a href="https://events.google.com/io/">Google I/O</a></li>
<li><a href="https://flutter.dev/docs/codelabs">Codelab</a></li>
<li><a href="https://medium.com/flutter/how-its-made-i-o-photo-booth-3b8355d35883">How It's Made</a></li>
<li><a href="https://policies.google.com/terms">Terms of Service</a></li>
<li><a href="https://policies.google.com/privacy">Privacy Policy</a></li>
</ul>
</div>
</footer>
<footer>
<div class="left">
<ul>
<li>
<a href="https://flutter.dev/">
<img class="flutter-icon" src={{assetUrls.flutterIcon}} />
</a>
</li>
<li>
<a href="https://firebase.google.com/">
<img class="firebase-icon" src={{assetUrls.firebaseIcon}} />
</a>
</li>
<li>
<a href="https://www.tensorflow.org/">
<img class="tensorflow-icon" src={{assetUrls.tensorflowIcon}} />
</a>
</li>
</ul>
</div>
<div class="right">
<ul>
<li><a href="https://flutter.dev/">Flutter</a></li>
<li><a href="https://firebase.google.com/">Firebase</a></li>
<li><a href="https://www.tensorflow.org/">TensorFlow</a></li>
<li><a href="https://mediapipe.dev/">MediaPipe</a></li>
<li><a href="https://policies.google.com/terms">Terms of Service</a></li>
<li><a href="https://policies.google.com/privacy">Privacy Policy</a></li>
</ul>
</div>
</footer>
`;

View File

@ -27,19 +27,12 @@ export default `
<style>{{{styles}}}</style>
</head>
<body>
<div class="backdrop"></div>
<img src="{{{assetUrls.fixedPhotosLeft}}}" class="fixed-photos left">
<img src="{{{assetUrls.fixedPhotosRight}}}" class="fixed-photos right">
<main>
<div class="share-image no-shadow">
<img src="{{{assetUrls.notFoundPhoto}}}">
</div>
<div class="text">
<h1>Taken with I/O Photo Booth</h1>
<h2>Oops! This photo is gone, but that doesn't mean the fun has to end.</h2>
<a class="share-btn" href="/">Take your own</a>
</div>
<body class="not-found-page">
<div class="not-found-backdrop"></div>
<main class="not-found-panel">
<h1>This page cant be found</h1>
<h2>Try going back or relaunch the app.</h2>
<a class="btn elevated-btn relaunch-btn" href="/">Relaunch HoloBooth</a>
</main>
{{{footer}}}
</body>

View File

@ -0,0 +1,49 @@
export default `
(function() {
const frame = document.querySelector('.card-frame');
const playButton = document.querySelector('.play-button');
const closeButton = document.querySelector('.close-button');
const videoPopUp = document.querySelector('.video-pop-up');
const backdrop = document.querySelector('.video-pop-up-backdrop');
const videoPlayer = document.querySelector('.video-pop-up video');
const timeCounter = document.querySelector('.time-counter');
const progressBar = document.querySelector('.video-progress-bar > div');
const fullscreenButton = document.querySelector('.fullscreen-button');
frame.addEventListener('click', function() {
backdrop.style.display = 'block';
videoPopUp.classList.remove('fullscreen');
videoPopUp.style.display = 'flex';
videoPlayer.play();
});
playButton.addEventListener('click', function() {
videoPlayer.play();
});
closeButton.addEventListener('click', function() {
backdrop.style.display = 'none';
videoPopUp.style.display = 'none';
});
videoPlayer.addEventListener('loadeddata', function() {
timeCounter.innerText = ['0:00 / 0:0', videoPlayer.duration].join('');
});
videoPlayer.addEventListener('timeupdate', function(event) {
timeCounter.innerText = [
'0:0',
Math.round(videoPlayer.currentTime),
' / 0:0',
videoPlayer.duration,
].join('');
const progress = videoPlayer.currentTime / videoPlayer.duration * 100;
progressBar.style.width = progress + '%';
});
fullscreenButton.addEventListener('click', function() {
videoPopUp.classList.toggle('fullscreen');
});
})();
`;

View File

@ -24,25 +24,82 @@ export default `
<meta name="twitter:site" content="@flutterdev">
<link href="https://fonts.googleapis.com/css?family=Google+Sans:400,500" rel="stylesheet">
<style>{{{styles}}}</style>
</head>
<body>
<div class="backdrop"></div>
<img src="{{{assetUrls.fixedPhotosLeft}}}" class="fixed-photos left">
<img src="{{{assetUrls.fixedPhotosRight}}}" class="fixed-photos right">
<main>
<div class="share-image">
<img src="{{{shareImageUrl}}}">
<div class="share-video">
<img class="holocard" src={{assetUrls.holocard}} />
<div class="card-frame">
<div class="video-clip">
<video src="{{{shareVideoUrl}}}"></video>
</div>
<img src={{assetUrls.videoFrame}} />
</div>
</div>
<div class="text">
<h1>Taken with I/O Photo Booth</h1>
<h2>Join the fun! Grab a photo with your favorite Google mascot
at the I/O Photo Booth.</h2>
<a class="share-btn" href="/">Get started</a>
<div class="info">
<div class="info-content">
<img class="flutter-forward-logo" src={{assetUrls.flutterForwardLogo}} />
<h1>Check it out my Flutter holocard!</h1>
<h2>
This video has been created with Flutter web app.
Create your unique video in a few steps:
</h2>
<a class="btn elevated-btn try-now-btn" href="http://holobooth.flutter.dev/">
<img src={{assetUrls.playArrowIcon}} />
Try now
</a>
<p class="disclaimer">
Your photo will be available at that URL for 30 days and then automatically deleted.
To request early deletion of your photo, please contact flutter-photo-booth@google.com and
be sure to include your unique URL in your request.
</p>
</div>
</div>
</main>
{{{footer}}}
<div class="video-pop-up-backdrop">
</div>
<div class="video-pop-up">
<div class="video-pop-up-screen">
<video src="{{{shareVideoUrl}}}"></video>
<a href="#" class="close-button">
<img src={{assetUrls.close}} />
</a>
</div>
<div class="video-progress-bar">
<div></div>
</div>
<div class="video-pop-up-controls">
<div class="left-side-controls">
<a href="#" class="player-btn play-button">
<img src={{assetUrls.playerPlay}} />
</a>
<span class="time-counter">
0:02 / 0:05
</span>
</div>
<div class="right-side-controls">
<a href="https://flutter.dev/" class="player-btn">
<img class="flutter-icon" src={{assetUrls.flutterIcon}} />
</a>
<a href="https://firebase.google.com/" class="player-btn">
<img class="firebase-icon" src={{assetUrls.firebaseIcon}} />
</a>
<a href="https://www.tensorflow.org/" class="player-btn">
<img class="tensorflow-icon" src={{assetUrls.tensorflowIcon}} />
</a>
<a href="#" class="player-btn fullscreen-button">
<img class="tensorflow-icon" src={{assetUrls.playerFullscreen}} />
</a>
</div>
</div>
</div>
</body>
<script type="text/javascript">
{{{scripts}}}
</script>
</html>
`;

View File

@ -25,221 +25,482 @@ body {
z-index: -1;
height: 100%;
width: 100%;
background-image: url("{{{assetUrls.bgMobile}}}");
background-image: url("{{{assetUrls.bg}}}");
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.fixed-photos {
position: fixed;
background-repeat: no-repeat;
background-size: contain;
z-index: 1;
display: none;
width: 780px;
main {
display: flex;
flex: 1;
flex-direction: row;
}
main {
width: 95%;
max-width: 1000px;
margin: 0 auto 45px;
text-align: center;
flex: 1 0 auto;
z-index: 10;
.share-video {
flex: 1;
position: relative;
}
.share-image {
margin: 2rem auto;
width: 90%;
transform: rotate(-5deg);
.info {
flex: 1;
display: flex;
align-items: center;
}
.share-image img {
width: 100%;
box-shadow: -3px 9px 7px 1px rgba(0, 0, 0, 0.3);
.info-content {
width: 524px;
display: inline-block;
}
.share-image.no-shadow img {
box-shadow: none;
}
h1, h2 {
color: white;
margin-block-start: 0;
margin-block-end: 0;
.flutter-forward-logo {
margin-top: 104px;
width: 330.13px;
height: 48px;
}
h1 {
line-height: 1.2;
font-size: 32px;
font-weight: 500;
width: 80%;
margin: 0 auto 15px;
font-family: 'Google Sans';
font-style: normal;
font-weight: 700;
font-size: 64px;
line-height: 80px;
background: linear-gradient(90deg, #F8BBD0 0%, #9E81EF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
h2 {
line-height: 1.3;
font-family: 'Google Sans';
font-style: normal;
font-weight: 400;
font-size: 18px;
font-weight: 100;
width: 85%;
margin: 0 auto 35px;
line-height: 24px;
color: #FFFFFF;
}
.share-btn {
display: inline-block;
font-weight: 400;
line-height: 1.5;
text-align: center;
text-decoration: none;
line-height: 1;
padding: 16px 0;
background-color: #428eff;
color: white;
.btn {
font-family: 'Google Sans';
font-style: normal;
font-weight: 500;
font-size: 18px;
line-height: 28px;
text-align: center;
color: #FFFFFF;
padding: 12px 40px 12px 20px;
width: 167px;
height: 52px;
text-decoration: none;
border-radius: 50px;
width: 208px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn img {
width: 24px;
height: 24px;
margin-right: 16px;
}
.elevated-btn {
background: linear-gradient(91.87deg, #4100E0 0.1%, #F8BBD0 120.1%);
}
.try-now-btn {
margin-right: 24px;
}
.outline-btn {
border: 1px solid #676AB6;
}
.disclaimer {
margin-top: 48px;
font-family: 'Google Sans';
font-style: italic;
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: #C0C0C0;
}
.card-frame {
width: 368px;
height: 467px;
position: absolute;
bottom: 206px;
left: 50%;
transform: translate(-50%, 0);
cursor: pointer;
}
.card-frame img {
width: 100%;
position: absolute;
}
.card-frame .video-clip {
height: 318px;
width: 286px;
top: 48px;
left: 50%;
position: absolute;
transform: translate(-50%, 0);
clip-path: inset(0);
}
.card-frame .video-clip video {
height: 318px;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
}
.holocard {
width: 708px;
height: 716px;
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%, 0);
}
footer {
width: 100%;
color: white;
font-size: 14px;
font-weight: 100;
flex-direction: column;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 5% 1.5rem;
text-align: center;
position: relative;
flex-shrink: 0;
z-index: 10;
position: relative;
}
footer a {
color: white;
text-decoration: none;
white-space: nowrap;
}
footer a:hover {
text-decoration: underline;
padding-right: 16px;
padding-bottom: 16px;
}
footer ul {
margin: 0;
padding: 0;
line-height: 2;
display: inline;
}
footer .left {
font-size: 16px;
font-weight: 400;
margin-bottom: 17px;
footer ul li {
list-style: none;
display: inline;
}
footer ul li img.flutter-icon {
width: 19px;
height: 22px;
}
footer ul li img.firebase-icon {
width: 24px;
height: 24px;
}
footer ul li img.tensorflow-icon {
width: 22px;
height: 24px;
}
footer .left a {
text-decoration: underline;
margin-right: 16px;
}
footer li {
display: inline;
margin-left: 1rem;
footer .right a {
font-family: 'Google Sans';
font-style: normal;
font-weight: 400;
font-size: 14px;
color: #FFFFFF;
line-height: 20px;
text-align: center;
margin-right: 24px;
text-decoration: none;
}
@media (min-width: 375px) {
h1 { width: 67%; }
h2 { width: 75%; }
.video-pop-up-backdrop {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
background: rgba(28, 32, 64, 0.8);
backdrop-filter: blur(8px);
display: none;
}
@media (min-width: 768px) {
.backdrop {
background-image: url("{{{assetUrls.bg}}}");
}
.video-pop-up {
position: fixed;
left: 50%;
top: 50%;
width: 80%;
transform: translate(-50%, -50%);
display: none;
background: rgba(2, 3, 32, 0.95);
backdrop-filter: blur(7.5px);
border-radius: 38px;
border: 1px solid #9E81EF;
flex-direction: column;
align-items: center;
padding: 16px;
}
.fixed-photos {
.video-pop-up.fullscreen {
width: 100%;
height: 100%;
border: none;
border-radius: 0;
}
.video-pop-up-screen {
position: relative;
flex: 1;
align-items: center;
display: flex;
}
.video-pop-up-screen video {
border-radius: 10px;
width: 100%;
}
.video-pop-up .close-button {
display: flex;
align-items: center;
justify-content: center;
width: 54px;
height: 54px;
border-radius: 27.5px;
background: rgba(4, 5, 34, 0.56);
top: 8px;
position: absolute;
right: 8px;
}
.video-pop-up .close-button img {
width: 34px;
height: 34px;
}
.video-pop-up .video-progress-bar {
background: #1E1E1E;
border-radius: 10px;
height: 8px;
width: 100%;
margin-top: 12px;
}
.video-pop-up .video-progress-bar > div {
background: #27F5DD;
border-radius: 10px;
height: 8px;
width: 0%;
}
.video-pop-up .player-btn {
text-decoration: none;
}
.video-pop-up-controls {
padding: 12px 0px;
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
}
.video-pop-up-controls > div {
display: flex;
align-items: center;
}
.video-pop-up-controls .left-side-controls {
}
.video-pop-up .play-button {
margin-right: 22px;
}
.video-pop-up .play-button img {
width: 18px;
height: 22px;
}
.video-pop-up .time-counter {
font-family: 'Google Sans';
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 20px;
color: #FFFFFF;
}
.right-side-controls .flutter-icon {
width: 18px;
height: 20px;
margin-right: 6px;
}
.right-side-controls .firebase-icon {
width: 22px;
height: 22px;
margin-right: 6px;
}
.right-side-controls .tensorflow-icon {
width: 20px;
height: 22px;
margin-right: 18px;
}
.right-side-controls .fullscreen-button img {
width: 30px;
height: 30px;
}
.not-found-backdrop {
z-index: -1;
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-image: url("{{{assetUrls.notFoundBg}}}");
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.not-found-panel {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.not-found-panel h1 {
font-size: 54px;
line-height: 64px;
margin-bottom: 24px;
}
.not-found-panel h2 {
margin-top: 0px;
margin-bottom: 40px;
}
.relaunch-btn {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10px 48px;
gap: 8px;
width: 286px;
height: 48px;
}
@media (max-width: 860px) {
body {
display: block;
}
.fixed-photos.left {
top: -151px;
left: -550px;
body.not-found-page {
display: flex;
}
.fixed-photos.right {
top: -110px;
right: -550px;
.not-found-backdrop {
background-image: url("{{{assetUrls.notFoundMobileBg}}}");
}
.share-image {
margin: 4.25rem auto 3rem;
position: relative;
left: -22px;
width: calc(100vh * 0.7);
max-width: 740px;
.not-found-panel {
padding: 24px 0;
}
h1, h2 {
width: 100%;
.not-found-panel h1 {
font-size: 34px;
line-height: 44px;
text-align: center;
}
h1 {
font-size: 44px;
.not-found-panel h2 {
font-size: 16px;
line-height: 24px;
text-align: center;
}
h2 {
font-size: 21px;
main {
display: block;
}
.share-btn {
font-size: 22px;
.share-video {
margin-top: 32px;
height: 464px;
}
.card-frame {
width: 218px;
height: 217px;
position: absolute;
bottom: 256px;
}
.card-frame .video-clip {
height: 218px;
width: 186px;
top: 14px;
}
.card-frame .video-clip video {
height: 318px;
position: absolute;
}
.holocard {
width: 608px;
height: 616px;
}
.info {
padding: 0px 24px;
}
.info .info-content {
width: auto;
display: flex;
flex-direction: column;
align-items: center;
}
.info .flutter-forward-logo {
margin-top: 0px;
}
.info h1 {
font-size: 34px;
line-height: 44px;
text-align: center;
}
.info h2 {
font-size: 16px;
text-align: center;
margin-bottom: 40px;
}
.try-now-btn {
margin-right: 0px;
margin-bottom: 24px;
}
.disclaimer {
margin-bottom: 86px;
}
footer {
flex-direction: row;
text-align: left;
padding-left: 2%;
padding-right: 2%;
justify-content: center;
}
footer .left {
font-size: 18px;
margin-bottom: 0;
}
}
@media (min-width: 992px) {
.fixed-photos.left {
left: -400px;
}
.fixed-photos.right {
right: -400px;
}
}
@media (min-width: 1200px) {}
@media (min-width: 1644px) {
h1 { font-size: 56px; }
h2 { font-size: 24px; }
}
@media (min-width: 1920px) {
.fixed-photos.left {
left: -150px;
}
.fixed-photos.right {
right: -150px;
}
.share-image {
margin-top: 9.5rem;
margin-bottom: 4rem;
footer .right {
display: none;
}
}
`;