Src
This commit is contained in:
parent
a6f09aaadc
commit
b73c849530
127
src/App.vue
Normal file
127
src/App.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div id="lunchinator" class="pt-4">
|
||||
<h1 class="text-center">Lunchinator</h1>
|
||||
</div>
|
||||
<div class="container p-4">
|
||||
<div class="row">
|
||||
<div class="col col-11">
|
||||
<DaySelection :selected=selectedDay :update-func="getRestaurantsForDay" />
|
||||
</div>
|
||||
<div class="col col-2 col-md-1">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row equal row-cols-1 row-cols-md-2 row-cols-lg-2 row-cols-xl-3">
|
||||
<Menu v-for="menu in menus" :menu=menu :swapFunction=swapRestaurants />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, onMounted, ref } from 'vue';
|
||||
import DaySelection from './components/DaySelection.vue'
|
||||
import Menu from './components/Menu.vue'
|
||||
import { Direction, Restaurant, RestaurantDay } from './types/Restaurant';
|
||||
import { restaurantToRestaurantDay } from './services/restaurantServices';
|
||||
import ThemeSelector from './components/ThemeSelector.vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { key } from './store';
|
||||
|
||||
const menus: Ref<RestaurantDay[]> = ref([]);
|
||||
const selectedDay: Ref<number> = ref(new Date().getDay());
|
||||
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||||
const baseUrl = 'https://lunch.zvon.tech/api';
|
||||
|
||||
const swapRestaurants = (id: number, direction: Direction) => {
|
||||
console.log("DOING");
|
||||
const oldIndex = store.state.restaurantOrder.get(id);
|
||||
if (oldIndex === undefined || oldIndex === null) {
|
||||
return;
|
||||
}
|
||||
console.log("DOING2");
|
||||
|
||||
const newIndex = oldIndex + (direction == Direction.LEFT ? -1 : 1);
|
||||
if(newIndex < 0 || newIndex >= menus.value.length) {
|
||||
return;
|
||||
}
|
||||
console.log("DOING3");
|
||||
|
||||
let swapId = -1;
|
||||
for (let [key, value] of store.state.restaurantOrder.entries()) {
|
||||
if (value === newIndex) {
|
||||
swapId = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const tmpRestaurant = menus.value[newIndex];
|
||||
menus.value[newIndex] = menus.value[oldIndex];
|
||||
menus.value[oldIndex] = tmpRestaurant;
|
||||
store.commit("updateRestaurantOrder", { id, newIndex, oldIndex, swapId });
|
||||
}
|
||||
|
||||
const updateRestaurantOrder = (restaurants: Restaurant[]) => {
|
||||
const restaurantOrder = new Map(store.state.restaurantOrder);
|
||||
for (const restaurant of restaurants) {
|
||||
if (!restaurantOrder.has(restaurant.id)) {
|
||||
restaurantOrder.set(restaurant.id, restaurantOrder.size);
|
||||
}
|
||||
}
|
||||
// TODO check if works?
|
||||
for (const [key, value] of restaurantOrder.entries()) {
|
||||
if (restaurants.filter(res => res.id == key).length === 0) {
|
||||
restaurantOrder.delete(key);
|
||||
for (const [key2, value2] of restaurantOrder.entries()) {
|
||||
if (value2 >= value) {
|
||||
restaurantOrder.set(key2, value2 - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
store.commit("setRestaurantOrder", restaurantOrder);
|
||||
}
|
||||
|
||||
const sortRestaurantDays = (restaurants: RestaurantDay[]): RestaurantDay[] => {
|
||||
const result = new Array<RestaurantDay>(restaurants.length);
|
||||
for (const restaurant of restaurants) {
|
||||
result[store.state.restaurantOrder.get(restaurant.id) ?? 0] = restaurant;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const getRestaurantsForDay = async (day: number) => {
|
||||
selectedDay.value = day;
|
||||
const response: Restaurant[] = await fetch(`${baseUrl}/get?day=${days[day]}`)
|
||||
.then(response => response.json());
|
||||
updateRestaurantOrder(response);
|
||||
menus.value = sortRestaurantDays(restaurantToRestaurantDay(response));
|
||||
}
|
||||
|
||||
getRestaurantsForDay(selectedDay.value);
|
||||
const store = useStore(key);
|
||||
|
||||
onMounted(() => {
|
||||
//document.querySelector("html")!.setAttribute('data-bs-theme', 'light');
|
||||
document.body.setAttribute('data-bs-theme', store.state.darkTheme ? 'dark' : 'light');
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./assets/scss/style.scss"
|
||||
</style>
|
35
src/assets/scss/style.scss
Normal file
35
src/assets/scss/style.scss
Normal file
@ -0,0 +1,35 @@
|
||||
// required to get $orange variable
|
||||
$primary-dark: #737378;
|
||||
$primary: #d8ba9c;
|
||||
$secondary: #737378;
|
||||
|
||||
@import "../../../node_modules/bootstrap/scss/functions";
|
||||
@import "../../../node_modules/bootstrap/scss/variables";
|
||||
@import "../../../node_modules/bootstrap/scss/variables-dark";
|
||||
@import "../../../node_modules/bootstrap/scss/maps";
|
||||
@import "../../../node_modules/bootstrap/scss/mixins";
|
||||
@import "../../../node_modules/bootstrap/scss/utilities";
|
||||
|
||||
//$card-cap-bg: rgba(var(--#{$prefix}emphasis-color), 1);
|
||||
:root,
|
||||
[data-bs-theme="light"] {
|
||||
--#{$prefix}custom-card-cap-bg: #{to-rgb($primary)};
|
||||
--#{$prefix}soup-bg: #{$primary};
|
||||
}
|
||||
:root,
|
||||
[data-bs-theme="dark"] {
|
||||
--#{$prefix}custom-card-cap-bg: #{to-rgb($primary-dark)};
|
||||
--#{$prefix}soup-bg: #{$primary-dark};
|
||||
}
|
||||
|
||||
$card-cap-bg: rgba(var(--#{$prefix}custom-card-cap-bg), .6);
|
||||
$pagination-active-bg: rgba(var(--#{$prefix}custom-card-cap-bg), 1);
|
||||
$pagination-active-border-color: rgba(var(--#{$prefix}custom-card-cap-bg), 1);
|
||||
$pagination-color: rgba(var(--#{$prefix}custom-card-cap-bg), 1);
|
||||
$pagination-hover-color: var(--#{prefix}card-cap-color);
|
||||
$form-check-input-checked-bg-color: rgba(var(--#{$prefix}custom-card-cap-bg), 1);
|
||||
$form-check-input-checked-border-color: rgba(var(--#{$prefix}custom-card-cap-bg), .5);
|
||||
$form-check-input-bg: rgba(var(--#{$prefix}custom-card-cap-bg), 1);
|
||||
|
||||
// set changes
|
||||
@import "../../../node_modules/bootstrap/scss/bootstrap";
|
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
37
src/components/DaySelection.vue
Normal file
37
src/components/DaySelection.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<nav aria-label="...">
|
||||
<ul class="pagination">
|
||||
<li :class="'page-item ' + (selectedDay == 1 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(1)">Po</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + (selectedDay == 2 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(2)">Út</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + (selectedDay == 3 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(3)">St</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + (selectedDay == 4 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(4)">Čt</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + (selectedDay == 5 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(5)">Pá</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + (selectedDay == 6 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(6)">So</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + (selectedDay == 0 ? 'active' : '')">
|
||||
<a href="#" class="page-link" @click="callFunc(0)">Ne</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{selected: number, updateFunc: (n: number) => void }>()
|
||||
let selectedDay = props.selected;
|
||||
|
||||
const callFunc = (day: number): void => {
|
||||
selectedDay = day;
|
||||
props.updateFunc(day);
|
||||
}
|
||||
</script>
|
105
src/components/Menu.vue
Normal file
105
src/components/Menu.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="col pt-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col col-10">
|
||||
{{ menu.restaurant }}
|
||||
</div>
|
||||
<div class="col col-1">
|
||||
<button class="btn w-100" @click="swapFunction(menu.id, Direction.LEFT)" style="padding-left: 0; border: none" :disabled="store.state.restaurantOrder.get(menu.id) === 0">
|
||||
<font-awesome-icon :icon="['fas', 'arrow-left']"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col col-1">
|
||||
<button class="btn w-100" @click="swapFunction(menu.id, Direction.RIGHT)" style="padding-left: 0; border: none" :disabled="store.state.restaurantOrder.get(menu.id) === store.state.restaurantOrder.size-1">
|
||||
<font-awesome-icon :icon="['fas', 'arrow-right']"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0px">
|
||||
<ul class="list-group">
|
||||
<li v-for="soup in menu.meals.filter(meal => meal.isSoup)" class="list-group-item active"
|
||||
style="border-radius: 0px; background-color: var(--bs-soup-bg); border-color: var(--bs-soup-bg);">
|
||||
<div class="row">
|
||||
<div class="col col-9">
|
||||
<span :data-bs-toggle="soup.description.length == 0 ? '' : 'tooltip'"
|
||||
data-bs-placement="top" :data-bs-title="soup.description">
|
||||
{{ soup.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col col-3 text-end">
|
||||
{{ soup.price < 0 ? '---' : soup.price }} Kč </div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="soup in menu.permanentmeals.filter(meal => meal.isSoup)" class="list-group-item active"
|
||||
style="border-radius: 0px; background-color: var(--bs-soup-bg); border-color: var(--bs-soup-bg);">
|
||||
<div class="row">
|
||||
<div class="col col-9">
|
||||
<span :data-bs-toggle="soup.description.length == 0 ? '' : 'tooltip'"
|
||||
data-bs-placement="top" :data-bs-title="soup.description">
|
||||
{{ soup.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col col-3 text-end">
|
||||
{{ soup.price < 0 ? '---' : soup.price }} Kč </div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="meal in menu.meals.filter(meal => !meal.isSoup)" class="list-group-item"
|
||||
style="border-radius: 0px">
|
||||
<div class="row">
|
||||
<div class="col col-9">
|
||||
<span :data-bs-toggle="meal.description.length == 0 ? '' : 'tooltip'"
|
||||
data-bs-placement="top" :data-bs-title="meal.description">
|
||||
{{ meal.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col col-3 text-end">
|
||||
{{ meal.price < 0 ? '---' : meal.price }} Kč </div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="meal in menu.permanentmeals.filter(meal => !meal.isSoup)" class="list-group-item"
|
||||
style="border-radius: 0px">
|
||||
<div class="row">
|
||||
<div class="col col-9">
|
||||
<span :data-bs-toggle="meal.description.length == 0 ? '' : 'tooltip'"
|
||||
data-bs-placement="top" :data-bs-title="meal.description">
|
||||
{{ meal.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col col-3 text-end">
|
||||
{{ meal.price < 0 ? '---' : meal.price }} Kč </div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import { onMounted, onUpdated } from 'vue';
|
||||
import { Direction, RestaurantDay } from '../types/Restaurant.ts'
|
||||
import { useStore } from 'vuex';
|
||||
import { key } from '../store';
|
||||
|
||||
const store = useStore(key);
|
||||
|
||||
defineProps<{ menu: RestaurantDay, swapFunction: (id: number, direction: Direction) => void }>()
|
||||
let tooltips: bootstrap.Tooltip[] = [];
|
||||
|
||||
onMounted(() => {
|
||||
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')]
|
||||
.forEach(el => tooltips.push(new bootstrap.Tooltip(el)));
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
tooltips.forEach(tooltip => { tooltip.dispose(); });
|
||||
tooltips = [];
|
||||
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')]
|
||||
.forEach(el => tooltips.push(new bootstrap.Tooltip(el)));
|
||||
})
|
||||
|
||||
</script>
|
18
src/components/ThemeSelector.vue
Normal file
18
src/components/ThemeSelector.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" :checked="store.state.darkTheme" @click="toggleDarkTheme">
|
||||
<label class="form-check-label" :style="{color: store.state.darkTheme ? 'var(--bs-body-color)' : 'var(--bs-primary)' }" for="flexSwitchCheckChecked"><font-awesome-icon :icon="['fas', store.state.darkTheme ? 'moon' : 'sun']" /></label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
import { key } from '../store';
|
||||
|
||||
const store = useStore(key);
|
||||
|
||||
const toggleDarkTheme = () => {
|
||||
store.commit("toggleDarkTheme");
|
||||
document.body.setAttribute('data-bs-theme', store.state.darkTheme ? 'dark' : 'light');
|
||||
}
|
||||
</script>
|
19
src/main.ts
Normal file
19
src/main.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
//import 'bootstrap/dist/css/bootstrap.css'
|
||||
//import 'font-awesome/css/font-awesome.min.css'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { faSun, faMoon, faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import App from './App.vue'
|
||||
import { store, key } from './store';
|
||||
|
||||
library.add(faSun);
|
||||
library.add(faMoon);
|
||||
library.add(faArrowLeft);
|
||||
library.add(faArrowRight);
|
||||
|
||||
const app = createApp(App);
|
||||
app.component('font-awesome-icon', FontAwesomeIcon)
|
||||
app.use(store, key);
|
||||
app.mount('#app');
|
23
src/services/restaurantServices.ts
Normal file
23
src/services/restaurantServices.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Meal, Restaurant, RestaurantDay } from "../types/Restaurant"
|
||||
|
||||
const noDataMeals: Meal[] = [
|
||||
{
|
||||
name: "No Data",
|
||||
description: "The selected restaurant has no data for this day",
|
||||
isSoup: false,
|
||||
price: -1
|
||||
}
|
||||
]
|
||||
|
||||
export const restaurantToRestaurantDay = (data: Restaurant[]): RestaurantDay[] => {
|
||||
const result = [];
|
||||
for(const restaurant of data) {
|
||||
result.push({
|
||||
id: restaurant.id,
|
||||
restaurant: restaurant.restaurant,
|
||||
meals: restaurant.dailymenus[0].meals.length == 0 && restaurant.permanentmeals.length == 0 ? noDataMeals : restaurant.dailymenus[0].meals,
|
||||
permanentmeals: restaurant.permanentmeals ?? [],
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
55
src/store.ts
Normal file
55
src/store.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { InjectionKey } from "vue";
|
||||
import { createStore, Store } from 'vuex';
|
||||
|
||||
export interface State {
|
||||
darkTheme: boolean
|
||||
restaurantOrder: Map<number, number>
|
||||
}
|
||||
|
||||
export const key: InjectionKey<Store<State>> = Symbol();
|
||||
|
||||
const restaurantOrderFromLocalStorage = (order: string | null): Map<number, number> => {
|
||||
const map = new Map<number, number>();
|
||||
if (!order) {
|
||||
return map;
|
||||
}
|
||||
const entries = order.split(',');
|
||||
for (let i = 0; i < entries.length; i += 2) {
|
||||
const key = parseInt(entries[i]);
|
||||
const value = parseInt(entries[i + 1]);
|
||||
map.set(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const storeRestaurantOrder = (order: Map<number, number>): void => {
|
||||
let resultString = "";
|
||||
for (const [key, value] of order.entries()) {
|
||||
resultString += key.toString() + "," + value.toString() + ","
|
||||
}
|
||||
resultString = resultString.substring(0, resultString.length - 1);
|
||||
window.localStorage.setItem("restaurantOrder", resultString);
|
||||
}
|
||||
|
||||
export const store = createStore<State>({
|
||||
state: {
|
||||
darkTheme: window.localStorage.getItem("darkTheme") === "true" ?? false,
|
||||
restaurantOrder: restaurantOrderFromLocalStorage(window.localStorage.getItem("restaurantOrder"))
|
||||
},
|
||||
mutations: {
|
||||
toggleDarkTheme(state: State) {
|
||||
state.darkTheme = !state.darkTheme;
|
||||
window.localStorage.setItem("darkTheme", state.darkTheme ? "true" : "false");
|
||||
},
|
||||
updateRestaurantOrder(state: State, payload: { id: number, newIndex: number, oldIndex: number, swapId: number }) {
|
||||
const { id, newIndex, oldIndex, swapId } = payload;
|
||||
state.restaurantOrder.set(swapId, oldIndex!);
|
||||
state.restaurantOrder.set(id, newIndex);
|
||||
storeRestaurantOrder(state.restaurantOrder);
|
||||
},
|
||||
setRestaurantOrder(state: State, payload: Map<number, number>) {
|
||||
state.restaurantOrder = payload
|
||||
storeRestaurantOrder(state.restaurantOrder);
|
||||
}
|
||||
}
|
||||
})
|
3
src/style.css
Normal file
3
src/style.css
Normal file
@ -0,0 +1,3 @@
|
||||
#lunchinator {
|
||||
font-family: 'Dancing Script', cursive;
|
||||
}
|
30
src/types/Restaurant.ts
Normal file
30
src/types/Restaurant.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export type Restaurant = {
|
||||
id: number
|
||||
restaurant: string
|
||||
dailymenus: Menu[]
|
||||
permanentmeals: Meal[]
|
||||
}
|
||||
|
||||
export type RestaurantDay = {
|
||||
id: number,
|
||||
restaurant: string
|
||||
meals: Meal[]
|
||||
permanentmeals: Meal[]
|
||||
}
|
||||
|
||||
export type Menu = {
|
||||
meals: Meal[]
|
||||
day: string
|
||||
}
|
||||
|
||||
export type Meal = {
|
||||
name: string
|
||||
description: string
|
||||
isSoup: boolean
|
||||
price: number
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
8
src/vite-env.d.ts
vendored
Normal file
8
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "vuex" {
|
||||
export * from "vuex/types/index.d.ts";
|
||||
export * from "vuex/types/helpers.d.ts";
|
||||
export * from "vuex/types/logger.d.ts";
|
||||
export * from "vuex/types/vue.d.ts";
|
||||
}
|
13
src/vuex.d.ts
vendored
Normal file
13
src/vuex.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import { Store } from 'vuex'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
// declare your own store states
|
||||
interface State {
|
||||
darkTheme: boolean
|
||||
}
|
||||
|
||||
// provide typings for `this.$store`
|
||||
interface ComponentCustomProperties {
|
||||
$store: Store<State>
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user