added card section card

This commit is contained in:
hayden 2021-01-25 07:54:49 -09:00
commit fd5a1672b0
14 changed files with 167 additions and 342 deletions

View file

@ -17,7 +17,7 @@
@selected="navigateFromSearch" @selected="navigateFromSearch"
/> />
</v-expand-x-transition> </v-expand-x-transition>
<v-btn icon @click="toggleSearch"> <v-btn icon @click="search = !search">
<v-icon>mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
</v-btn> </v-btn>
@ -34,11 +34,11 @@
</template> </template>
<script> <script>
import Menu from "./components/UI/Menu" import Menu from "./components/UI/Menu";
import SearchBar from "./components/UI/SearchBar" import SearchBar from "./components/UI/SearchBar";
import AddRecipeFab from "./components/UI/AddRecipeFab" import AddRecipeFab from "./components/UI/AddRecipeFab";
import SnackBar from "./components/UI/SnackBar" import SnackBar from "./components/UI/SnackBar";
import Vuetify from "./plugins/vuetify" import Vuetify from "./plugins/vuetify";
export default { export default {
name: "App", name: "App",
@ -51,16 +51,16 @@ export default {
watch: { watch: {
$route() { $route() {
this.search = false this.search = false;
}, },
}, },
mounted() { mounted() {
this.$store.dispatch("initTheme") this.$store.dispatch("initTheme");
this.$store.dispatch("requestRecentRecipes") this.$store.dispatch("requestRecentRecipes");
this.$store.dispatch("initLang") this.$store.dispatch("initLang");
this.darkModeSystemCheck() this.darkModeSystemCheck();
this.darkModeAddEventListener() this.darkModeAddEventListener();
}, },
data: () => ({ data: () => ({
@ -74,30 +74,22 @@ export default {
if (this.$store.getters.getDarkMode === "system") if (this.$store.getters.getDarkMode === "system")
Vuetify.framework.theme.dark = window.matchMedia( Vuetify.framework.theme.dark = window.matchMedia(
"(prefers-color-scheme: dark)" "(prefers-color-scheme: dark)"
).matches ).matches;
}, },
/** /**
* This will monitor the OS level darkmode and call to update dark mode. * This will monitor the OS level darkmode and call to update dark mode.
*/ */
darkModeAddEventListener() { darkModeAddEventListener() {
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
darkMediaQuery.addEventListener("change", () => { darkMediaQuery.addEventListener("change", () => {
this.darkModeSystemCheck() this.darkModeSystemCheck();
}) });
},
toggleSearch() {
if (this.search === true) {
this.search = false
} else {
this.search = true
}
}, },
navigateFromSearch(slug) { navigateFromSearch(slug) {
this.$router.push(`/recipe/${slug}`) this.$router.push(`/recipe/${slug}`);
}, },
}, },
} };
</script> </script>
<style> <style>

View file

@ -1,79 +0,0 @@
<template>
<v-card-text>
<p>
{{
$t(
"migration.currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration"
)
}}
</p>
<v-form ref="form">
<v-row dense align="center">
<v-col cols="12" md="5" sm="5">
<v-text-field
v-model="repo"
:label="$t('migration.chowdown-repo-url')"
:rules="[rules.required]"
>
</v-text-field>
</v-col>
<v-col cols="12" md="4" sm="5">
<v-btn text color="info" @click="importRepo">
<v-icon left> mdi-import </v-icon>
{{ $t("migration.migrate") }}
</v-btn>
</v-col>
</v-row>
</v-form>
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
<h4>{{ $t("migration.failed-recipes") }}</h4>
<v-list dense>
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
{{ fail }}
</v-list-item>
</v-list>
</v-alert>
<v-alert v-if="failedImages[1]" outlined dense type="error">
<h4>{{ $t("migration.failed-images") }}</h4>
<v-list dense>
<v-list-item v-for="fail in this.failedImages" :key="fail">
{{ fail }}
</v-list-item>
</v-list>
</v-alert>
</v-card-text>
</template>
<script>
import api from "../../../api";
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
// import TimePicker from "./Webhooks/TimePicker";
export default {
data() {
return {
processRan: false,
failedImages: [],
failedRecipes: [],
repo: "",
rules: {
required: v => !!v || "Selection Required",
},
};
},
methods: {
async importRepo() {
if (this.$refs.form.validate()) {
this.$emit("loading");
let response = await api.migrations.migrateChowdown(this.repo);
this.failedImages = response.failedImages;
this.failedRecipes = response.failedRecipes;
this.$emit("finished");
this.processRan = true;
}
},
},
};
</script>
<style>
</style>

View file

@ -1,112 +0,0 @@
<template>
<v-card-text>
<p>
{{
$t(
"migration.you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected"
)
}}
</p>
<v-form ref="form">
<v-row align="center">
<v-col cols="12" md="5" sm="12">
<v-select
:items="availableImports"
v-model="selectedImport"
:label="$t('migration.nextcloud-data')"
:rules="[rules.required]"
></v-select>
</v-col>
<v-col md="1" sm="12">
<v-btn-toggle group>
<v-btn text color="info" @click="importRecipes">
<v-icon left> mdi-import </v-icon>
{{ $t("migration.migrate") }}
</v-btn>
<v-btn text color="error" @click="deleteImportValidation">
<v-icon left> mdi-delete </v-icon>
{{ $t("general.delete") }}
</v-btn>
<UploadBtn
url="/api/migration/upload/"
class="mt-1"
@uploaded="getAvaiableImports"
/>
<Confirmation
:title="$t('general.delete-data')"
:message="$t('migration.delete-confirmation')"
color="error"
icon="mdi-alert-circle"
ref="deleteThemeConfirm"
v-on:confirm="deleteImport()"
/>
</v-btn-toggle>
</v-col>
<v-spacer></v-spacer>
</v-row>
</v-form>
<SuccessFailureAlert
:success-header="$t('migration.successfully-imported-from-nextcloud')"
:success="successfulImports"
failed-header="$t('migration.failed-imports')"
:failed="failedImports"
/>
</v-card-text>
</template>
<script>
import api from "../../../api";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import Confirmation from "../../UI/Confirmation";
import UploadBtn from "../../UI/UploadBtn";
export default {
components: {
SuccessFailureAlert,
Confirmation,
UploadBtn,
},
data() {
return {
successfulImports: [],
failedImports: [],
availableImports: [],
selectedImport: null,
rules: {
required: v => !!v || "Selection Required",
},
};
},
async mounted() {
this.getAvaiableImports();
},
methods: {
async getAvaiableImports() {
this.availableImports = await api.migrations.getNextcloudImports();
},
async importRecipes() {
if (this.$refs.form.validate()) {
this.$emit("loading");
let data = await api.migrations.importNextcloud(this.selectedImport);
this.successfulImports = data.successful;
this.failedImports = data.failed;
this.$emit("finished");
}
},
deleteImportValidation() {
if (this.$refs.form.validate()) {
this.$refs.deleteThemeConfirm.open();
}
},
async deleteImport() {
await api.migrations.delete(this.selectedImport);
this.getAvaiableImports();
},
},
};
</script>
<style>
</style>

View file

@ -1,49 +0,0 @@
<template>
<v-form ref="file">
<v-file-input
:loading="loading"
:label="$t('migration.upload-an-archive')"
v-model="file"
accept=".zip"
@change="upload"
:prepend-icon="icon"
class="file-icon"
>
</v-file-input>
</v-form>
</template>c
<script>
import api from "../../../api";
export default {
data() {
return {
file: null,
loading: false,
icon: "mdi-paperclip",
};
},
methods: {
async upload() {
if (this.file != null) {
this.loading = true;
let formData = new FormData();
formData.append("archive", this.file);
await api.migrations.uploadFile(formData);
this.loading = false;
this.$emit("uploaded");
this.file = null;
this.icon = "mdi-check";
}
},
},
};
</script>
<style>
.file-icon {
transition-duration: 5s;
}
</style>

View file

@ -0,0 +1,66 @@
<template>
<div class="mt-n5">
<v-card flat class="transparent mb-2" height="50px">
<v-card-text>
<v-row>
<v-col>
<v-btn-toggle group>
<v-btn text :to="`/recipes/category/${title.toLowerCase()}`">
{{ title.toUpperCase() }}
</v-btn>
</v-btn-toggle>
</v-col>
<v-spacer></v-spacer>
<v-col align="end">
<v-btn-toggle group>
<v-btn text color="accent"> Sort </v-btn>
<v-btn text color="accent"> Limit </v-btn>
</v-btn-toggle>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-row>
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="recipe in recipes.slice(0, cardLimit)"
:key="recipe.name"
>
<RecipeCard
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import RecipeCard from "./RecipeCard";
export default {
components: {
RecipeCard,
},
props: {
title: String,
recipes: Array,
},
data() {
return {
cardLimit: 6,
};
},
};
</script>
<style>
.transparent {
opacity: 1;
}
</style>

View file

@ -1,40 +0,0 @@
<template>
<v-row>
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="recipe in recipes"
:key="recipe.name"
>
<RecipeCard
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
/>
</v-col>
</v-row>
</template>
<script>
import RecipeCard from "./RecipeCard";
export default {
components: {
RecipeCard,
},
data: () => ({}),
mounted() {},
computed: {
recipes() {
return this.$store.getters.getRecentRecipes;
},
},
};
</script>
<style scoped>
</style>

View file

@ -1,15 +1,45 @@
<template> <template>
<div> <div>
<RecentRecipes /> <CardSection v-if="showRecent" title="Recent" :recipes="recentRecipes" />
<CardSection
v-for="section in recipeByCategory"
:key="section.title"
:title="section.title"
:recipes="section.recipes"
/>
</div> </div>
</template> </template>
<script> <script>
import RecentRecipes from "../components/UI/RecentRecipes"; import CardSection from "../components/UI/CardSection";
export default { export default {
components: { components: {
RecentRecipes, CardSection,
},
data() {
return {
showRecent: true,
recipeByCategory: [
{
title: "Title 1",
recipes: this.$store.getters.getRecentRecipes,
},
{
title: "Title 2",
recipes: this.$store.getters.getRecentRecipes,
},
],
};
},
computed: {
recentRecipes() {
return this.$store.getters.getRecentRecipes;
},
},
methods: {
getRecentRecipes() {
this.$store.dispatch("requestRecentRecipes");
},
}, },
}; };
</script> </script>

View file

@ -3,7 +3,7 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
# import utils.startup as startup # import utils.startup as startup
from app_config import PORT, PRODUCTION, SQLITE_FILE, WEB_PATH, docs_url, redoc_url from app_config import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
from routes import ( from routes import (
backup_routes, backup_routes,
meal_routes, meal_routes,
@ -26,7 +26,6 @@ app = FastAPI(
) )
def mount_static_files(): def mount_static_files():
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True)) app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))

View file

@ -1,3 +1,5 @@
from sqlalchemy.orm.session import Session
from db.db_base import BaseDocument from db.db_base import BaseDocument
from db.sql.meal_models import MealPlanModel from db.sql.meal_models import MealPlanModel
from db.sql.recipe_models import RecipeModel from db.sql.recipe_models import RecipeModel
@ -16,8 +18,12 @@ class _Recipes(BaseDocument):
self.primary_key = "slug" self.primary_key = "slug"
self.sql_model = RecipeModel self.sql_model = RecipeModel
def update_image(self, slug: str, extension: str) -> None: def update_image(self, session: Session, slug: str, extension: str) -> str:
pass entry = self._query_one(session, match_value=slug)
entry.image = f"{slug}.{extension}"
session.commit()
return f"{slug}.{extension}"
class _Meals(BaseDocument): class _Meals(BaseDocument):
@ -31,7 +37,7 @@ class _Settings(BaseDocument):
self.primary_key = "name" self.primary_key = "name"
self.sql_model = SiteSettingsModel self.sql_model = SiteSettingsModel
def save_new(self, session, main: dict, webhooks: dict) -> str: def save_new(self, session: Session, main: dict, webhooks: dict) -> str:
new_settings = self.sql_model(main.get("name"), webhooks) new_settings = self.sql_model(main.get("name"), webhooks)
session.add(new_settings) session.add(new_settings)
@ -45,14 +51,6 @@ class _Themes(BaseDocument):
self.primary_key = "name" self.primary_key = "name"
self.sql_model = SiteThemeModel self.sql_model = SiteThemeModel
def update(self, session, data: dict) -> dict:
theme_model = self._query_one(
session=session, match_value=data["name"], match_key="name"
)
theme_model.update(**data)
session.commit()
class Database: class Database:
def __init__(self) -> None: def __init__(self) -> None:

View file

@ -1,4 +1,4 @@
from typing import Union from typing import List, Union
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -11,7 +11,10 @@ class BaseDocument:
self.store: str self.store: str
self.sql_model: SqlAlchemyBase self.sql_model: SqlAlchemyBase
def get_all(self, session: Session, limit: int = None, order_by: str = None): # TODO: Improve Get All Query Functionality
def get_all(
self, session: Session, limit: int = None, order_by: str = None
) -> List[dict]:
list = [x.dict() for x in session.query(self.sql_model).all()] list = [x.dict() for x in session.query(self.sql_model).all()]
if limit == 1: if limit == 1:
@ -21,7 +24,7 @@ class BaseDocument:
def _query_one( def _query_one(
self, session: Session, match_value: str, match_key: str = None self, session: Session, match_value: str, match_key: str = None
) -> Union[Session, SqlAlchemyBase]: ) -> SqlAlchemyBase:
"""Query the sql database for one item an return the sql alchemy model """Query the sql database for one item an return the sql alchemy model
object. If no match key is provided the primary_key attribute will be used. object. If no match key is provided the primary_key attribute will be used.
@ -43,7 +46,7 @@ class BaseDocument:
def get( def get(
self, session: Session, match_value: str, match_key: str = None, limit=1 self, session: Session, match_value: str, match_key: str = None, limit=1
) -> dict or list[dict]: ) -> dict or List[dict]:
"""Retrieves an entry from the database by matching a key/value pair. If no """Retrieves an entry from the database by matching a key/value pair. If no
key is provided the class objects primary key will be used to match against. key is provided the class objects primary key will be used to match against.
@ -67,6 +70,15 @@ class BaseDocument:
return db_entry return db_entry
def save_new(self, session: Session, document: dict) -> dict: def save_new(self, session: Session, document: dict) -> dict:
"""Creates a new database entry for the given SQL Alchemy Model.
Args:
session (Session): A Database Session
document (dict): A python dictionary representing the data structure
Returns:
dict: A dictionary representation of the database entry
"""
new_document = self.sql_model(**document) new_document = self.sql_model(**document)
session.add(new_document) session.add(new_document)
return_data = new_document.dict() return_data = new_document.dict()
@ -74,7 +86,18 @@ class BaseDocument:
return return_data return return_data
def update(self, session: Session, match_value, new_data) -> dict: def update(self, session: Session, match_value: str, new_data: str) -> dict:
"""Update a database entry.
Args:
session (Session): Database Session
match_value (str): Match "key"
new_data (str): Match "value"
Returns:
dict: Returns a dictionary representation of the database entry
"""
entry = self._query_one(session=session, match_value=match_value) entry = self._query_one(session=session, match_value=match_value)
entry.update(session=session, **new_data) entry.update(session=session, **new_data)
return_data = entry.dict() return_data = entry.dict()

View file

@ -12,7 +12,7 @@ class SiteThemeModel(SqlAlchemyBase):
self.name = name self.name = name
self.colors = ThemeColorsModel(**colors) self.colors = ThemeColorsModel(**colors)
def update(self, name, colors: dict) -> dict: def update(self, session=None, name: str = None, colors: dict = None) -> dict:
self.colors.update(**colors) self.colors.update(**colors)
return self.dict() return self.dict()

View file

@ -68,7 +68,6 @@ def get_recipe_img(recipe_slug: str):
return FileResponse(recipe_image) return FileResponse(recipe_image)
# Recipe Creations
@router.post( @router.post(
"/api/recipe/create-url/", "/api/recipe/create-url/",
status_code=201, status_code=201,

View file

@ -82,15 +82,6 @@ class Recipe(BaseModel):
slug = calc_slug slug = calc_slug
return slug return slug
@classmethod
def _unpack_doc(cls, document):
document = json.loads(document.to_json())
del document["_id"]
document["dateAdded"] = document["dateAdded"]["$date"]
return cls(**document)
@classmethod @classmethod
def get_by_slug(cls, session, slug: str): def get_by_slug(cls, session, slug: str):
""" Returns a Recipe Object by Slug """ """ Returns a Recipe Object by Slug """
@ -132,8 +123,15 @@ class Recipe(BaseModel):
return updated_slug.get("slug") return updated_slug.get("slug")
@staticmethod @staticmethod
def update_image(slug: str, extension: str): def update_image(slug: str, extension: str) -> str:
db.recipes.update_image(slug, extension) """A helper function to pass the new image name and extension
into the database.
Args:
slug (str): The current recipe slug
extension (str): the file extension of the new image
"""
return db.recipes.update_image(slug, extension)
@staticmethod @staticmethod
def get_all(session: Session): def get_all(session: Session):

View file

@ -103,7 +103,7 @@ class SiteTheme(BaseModel):
db.themes.save_new(session, self.dict()) db.themes.save_new(session, self.dict())
def update_document(self, session: Session): def update_document(self, session: Session):
db.themes.update(session, self.dict()) db.themes.update(session, self.name, self.dict())
@staticmethod @staticmethod
def delete_theme(session: Session, theme_name: str) -> str: def delete_theme(session: Session, theme_name: str) -> str: