Skip to content

Instantly share code, notes, and snippets.

@sashachabin
Last active November 21, 2023 08:27
Show Gist options
  • Save sashachabin/b645837dc19c546035d2da42027c4196 to your computer and use it in GitHub Desktop.
Save sashachabin/b645837dc19c546035d2da42027c4196 to your computer and use it in GitHub Desktop.
Веб-версия простейшнго меню для бара (pushinbar.ru)

Сайт барного меню

MVP-версия сайта с меню бара pushinbar.ru, сделанная Сашей Чабиным в барном мастер-классе за полтора часа.

image

  • База данных в Google Таблице
  • Минимальный Backend на Google App Script
  • Простейший Frontend на ванильных HTML/CSS/JS
  • Бесплатный хостинг и CI от Vercel

Данные в Google Таблице

Информация о пиве и его таблицы управляется прямо и Google Таблицы: https://docs.google.com/spreadsheets/d/1x2GUc8vCtzdQ8SdqTswv3E-NidNZXEXzqsLY6z9JGQA/edit#gid=1562855295

image

Бэкэнд на Google AppScript (код из интерфейса Google AppScript)

Сервер script.googleusercontent.com возвращает данные в формате JSON:

[
  {
    "brewery": "Jaws",
    "availability": false,
    "beer": "Атомная Прачечная IPA",
    "type1": "Ипа",
    "type2": "",
    "volume": "0.5",
    "price": "280",
    "cover": "https://untappd.akamaized.net/site/beer_logos_hd/beer-631746_0b8c5_hd.jpeg"
  },
  ...
]

Верстка страницы

  • Мета-информация
  • Логотип
  • Подключение стилей и скриптов
  • Контейнер для меню бара
  • Подвал

Стили для страницы. Для наименования классов используется БЭМ — один из самых простых и удобных способов организации CSS

.block {}
.block__element {}
.block__element--modifier {}

Код для динамической подгрузке данных из backend-google-app.gs

Хостинг

  • Бесплатный хостинг от Vercel с инеграцией для Github. Ты просто делаешь push, и ветка сразу выкладывается на тестовый поддомен. Кстати, тут есть Serveless с Nodejs, Python и PHP. А ещё можно бесплатно собрать и более серьезный фронтэнд с React/Vue/Angular и Webpack/Parcel/ваша-любимая-технология.
  • Домен за 145 рублей в год от Atex.ru. Мониторить дешевых регистраторов удобно в теме древнейшего форума

Как сделать несерьезное посерьезнее?

База побыстрее. После того как вы побаловались с выводом и вас перестала устраивать не самая быстрая скорость Google AppScript, то данные из гугл-таблички следует кэшировать в какой-нибудь бесплатной облачной базе данных Firebase. Например, можно глянуть на статью Sync Google Sheets to a Firebase Realtime Database.

CMS поумнее. Дальше интерфейс Google Таблиц может перестать вас устраивать и вам понадобится CMS. Для этого необязательно брать WordPress и Joomla и писать на PHP, есть неплохая альтернатива — Headless CMS. Напрмер, это Strapi и Directus — написаны на Nodejs возвращают JSON-ки вместо HTML, как наш бэкэнд. Вообще их тысячи на самых разных языках, все они собраны на headlesscms.org.

Интерфейс посложнее. Если интерфейс становится более интерактивным и динамическим, то пригодятся какие-нибудь фреймворки React/Vue/Angular, менеджер состояний Redux/MobX, ну а там и система сборки Webpack/Parcel. А ещё в моем примере мы рендерим всё на клиенте через fetch и innerHTML, однако поисковики просят рендеринг на стороне сервера для SEO (эх, даже в 2022). Здесь на помощь приходит крутое коробочное решение — NextJS с React и рендерингом на стороне сервера. Альтернативы для Nextjs можно найти на jamstack.org.

// Бэкэнд на Google AppScript на языке GoogleScript (как и JavaScript работает на движке V8)
// Получение данных из Google Таблицы
// Создать новый Script: https://script.google.com/home/projects/create
const GOOGLE_SPEADSHEET_ID = "1x2GUc8vCtzdQ8SdqTswv3E-NidNZXEXzqsLY6z9JGQA";
const MIN_ROW_NUMBER = 3;
const MIN_COLUMN_NUMBER = 1;
function doGet() {
return ContentService.createTextOutput(JSON.stringify(getBeerMenuData())).setMimeType(ContentService.MimeType.JSON);
}
function getBeerMenuData() {
const ss = SpreadsheetApp.openById(GOOGLE_SPEADSHEET_ID);
const sheet = ss.getSheetByName("Меню");
const maxRows = sheet.getMaxRows();
const maxColumns = sheet.getMaxColumns();
const range = sheet.getRange(MIN_ROW_NUMBER, MIN_COLUMN_NUMBER, maxRows, maxColumns).getValues();
const result = range.map(([brewery, enabled, beer, type1, type2, volume, price, , cover]) => {
return {
brewery,
enabled,
beer,
type1,
type2,
volume,
price,
cover
};
}).filter(item => item.enabled);
return result;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Барное меню</title>
<!-- Подключение стилей -->
<link rel="stylesheet" href="./style.css">
<meta name="description" content="Бар в Контуре на Малопрудной, 5" />
<!-- Черная адресная строка в мобильных браузерах -->
<meta name="theme-color" content="#000000">
</head>
<body>
<main class="wrapper">
<h1 class="logo">Push In Bar</h1>
<div class="menu" id="menu">Загружаем пиво...</div>
<footer class="footer">
<p class="footer__logo">Push In Bar</p>
<p class="footer__text">Барный бар с пивным пивом в контурном Контуре</p>
<p class="footer__text">Екатеринбург, Малопрудная, 5</p>
<a class="footer__link" href="https://www.instagram.com/push_inbar/" target="_blank" rel="noopener noreferrer">Инстаграм</a>
<br />
<a class="footer__link" href="https://youtu.be/QfjtPUjU-oQ" target="_blank" rel="noopener noreferrer">Подкаст</a>
</footer>
</main>
<!-- Подключение скрипта -->
<script src="script.js"></script>
</body>
</html>
(async () => {
const response = await fetch('https://script.googleusercontent.com/macros/echo?user_content_key=Qo3nCev3vKhzejCjIcVZhB3ULyuCcBbL96mT4beg5cEbpTrLIM9I2Vz2-MRljh3dZB7UVyrrKwBWI-HvYVs3EWLTaVdQp0jPm5_BxDlH2jW0nuo2oDemN9CCS2h10ox_1xSncGQajx_ryfhECjZEnCtUk43f48yQC-4h6uPTT3F5OK0fJemEGBaC-lLKqKzy2Q9eHLyJ9qux9rcQPyY6WCG_-W_z8TVH3c_8bZg2_Bdf-wvr4dxwbdz9Jw9Md8uu&lib=MAmgsdUMg_-ZrqH71iCQ13b_P0nMP0Yb0');
const data = await response.json();
const menu = document.getElementById('menu');
menu.innerHTML = data.map(({ brewery, enabled, beer, type1, type2, volume, price, cover }) => {
return `
<article class="item">
<div class="item__cover-container">
<img class="item__cover" src="${cover}" alt="">
<div class="item__price">${price} ₽</div>
</div>
<h3 class="item__beer">${beer}</h3>
<div class="item__volume">${volume}</div>
</article>
`;
}).join('');
})();
body,
html {
margin: 0;
padding: 0;
}
body {
font-family: sans-serif;
background: black;
color: white;
}
.wrapper {
max-width: 600px;
margin: 0 auto;
}
.logo {
margin-bottom: 48px;
padding: 0 8px;
text-align: center;
}
.menu {
margin: 0 auto;
padding: 0 8px;
min-height: 100vh;
text-align: center;
}
.item {
position: relative;
margin-bottom: 48px;
}
.item__beer {
font-size: 20px;
line-height: 1.2;
margin-top: 32px;
margin-bottom: 0;
}
.item__volume {
font-size: 24px;
line-height: 1.2;
.item__type {
position: absolute;
top: 0;
left: 0;
transform: rotate(2deg);
}
.item__cover-container {
position: relative;
text-align: center;
}
.item__price {
position: absolute;
right: 0;
bottom: 0;
background: white;
color: black;
padding: 10px;
font-size: 25px;
transform: rotate(2.99deg);
transform-origin: left bottom;
margin-top: 8px;
/* Отгрызаем углы. Делаем цену, почти как в дизайне */
clip-path: polygon(40% 0, 0 0, 0 46%, 0 100%, 44% 100%, 50% 89%, 57% 100%, 100% 100%, 100% 51%, 100% 0, 55% 0, 47% 10%);
}
.item__cover {
width: 93%;
/* Задаем соотношение сторон, чтобы высота высчитывалась автоматически */
aspect-ratio: 109 / 60;
transform-origin: bottom;
transform: rotate(-4deg);
border-radius: 16px;
object-fit: cover;
object-position: center;
}
.footer {
font-size: 16px;
background: #310D44;
border-radius: 16px 16px 0px 0px;
padding: 4px 24px 32px 24px;
}
.footer__logo {
font-size: 32px;
font-weight: 600;
}
.footer__text {
margin-bottom: 24px;
}
.footer__link {
display: inline-block;
margin-bottom: 8px;
color: white;
text-decoration: none;
}
.footer__link:hover {
text-decoration: underline;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment