This commit is contained in:
zv0n 2023-08-27 18:40:11 +02:00
parent a6f09aaadc
commit b73c849530
13 changed files with 474 additions and 0 deletions

127
src/App.vue Normal file
View 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>

View 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
View 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

View 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)"></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
View 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 }} </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 }} </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 }} </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 }} </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>

View 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
View 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');

View 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
View 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
View File

@ -0,0 +1,3 @@
#lunchinator {
font-family: 'Dancing Script', cursive;
}

30
src/types/Restaurant.ts Normal file
View 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
View 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
View 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>
}
}