Handle pictures
continuous-integration/drone/push Build is running
Details
continuous-integration/drone/push Build is running
Details
This commit is contained in:
parent
dcc90aa6af
commit
fcbaca79ea
|
@ -2,18 +2,20 @@ package api
|
|||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"git.nemunai.re/nemunaire/gustus/config"
|
||||
"git.nemunai.re/nemunaire/gustus/filestorage"
|
||||
"git.nemunai.re/nemunaire/gustus/model"
|
||||
"git.nemunai.re/nemunaire/gustus/storage"
|
||||
)
|
||||
|
||||
func declareRecipesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
router.GET("/random_recipe", RandomRecipe)
|
||||
router.GET("/recipes", ListRecipes)
|
||||
router.POST("/recipes", NewRecipe)
|
||||
|
||||
|
@ -28,16 +30,31 @@ func declareRecipesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
declareRecipePicturesRoutes(cfg, apiRecipeRoutes)
|
||||
}
|
||||
|
||||
func RandomRecipe(c *gin.Context) {
|
||||
type DisplayRecipe struct {
|
||||
*gustus.Recipe
|
||||
MainPicture string `json:"picture,omitempty"`
|
||||
}
|
||||
|
||||
func (dr *DisplayRecipe) GetMainPicture(c *gin.Context) error {
|
||||
storage := c.MustGet("storage").(*storage.Database)
|
||||
|
||||
recipes, err := storage.GetRecipes()
|
||||
pictures, err := storage.GetPicturesRelatedToRecipe(dr.Recipe)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to list recipes. Please retry later."})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, recipes[0])
|
||||
if len(pictures) > 0 {
|
||||
filestorage := c.MustGet("filestorage").(filestorage.FileStorage)
|
||||
|
||||
url, err := filestorage.GetURL(pictures[0], ".jpg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dr.MainPicture = url.String()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListRecipes(c *gin.Context) {
|
||||
|
@ -53,7 +70,33 @@ func ListRecipes(c *gin.Context) {
|
|||
if len(recipes) == 0 {
|
||||
c.JSON(http.StatusOK, []interface{}{})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, recipes)
|
||||
if c.Request.URL.Query().Has("random") {
|
||||
for i := range recipes {
|
||||
j := rand.Intn(i + 1)
|
||||
recipes[i], recipes[j] = recipes[j], recipes[i]
|
||||
}
|
||||
}
|
||||
|
||||
if limit, err := strconv.Atoi(c.Query("limit")); err != nil && limit > 0 {
|
||||
recipes = recipes[:limit]
|
||||
} else if limit, err := strconv.Atoi(c.Query("rlimit")); err != nil && limit > 0 {
|
||||
recipes = recipes[len(recipes)-limit:]
|
||||
}
|
||||
|
||||
var drs []DisplayRecipe
|
||||
for _, recipe := range recipes {
|
||||
dr := DisplayRecipe{
|
||||
recipe,
|
||||
"",
|
||||
}
|
||||
err := dr.GetMainPicture(c)
|
||||
if err != nil {
|
||||
log.Println("Unable to retrieve MainPicture of the recipe:", recipe.ID, err)
|
||||
}
|
||||
drs = append(drs, dr)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, drs)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +149,18 @@ func RecipeHandler(c *gin.Context) {
|
|||
|
||||
func GetRecipe(c *gin.Context) {
|
||||
recipe := c.MustGet("recipe").(*gustus.Recipe)
|
||||
c.JSON(http.StatusOK, recipe)
|
||||
|
||||
dr := DisplayRecipe{
|
||||
recipe,
|
||||
"",
|
||||
}
|
||||
err := dr.GetMainPicture(c)
|
||||
if err != nil {
|
||||
log.Println("Unable to retrive MainPicture of recipe:", recipe.ID, err)
|
||||
c.JSON(http.StatusOK, recipe)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, dr)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateRecipe(c *gin.Context) {
|
||||
|
|
|
@ -11,7 +11,7 @@ type Picture struct {
|
|||
ID primitive.ObjectID `bson:"_id" json:"id"`
|
||||
RecipeID primitive.ObjectID `json:"recipe_id"`
|
||||
AuthorID primitive.ObjectID `json:"author_id"`
|
||||
Label string `json:"name,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
UploadDate time.Time `json:"upload_date"`
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
import IngredientCard from '../components/IngredientCard.svelte';
|
||||
import Textarea from '../components/Textarea.svelte';
|
||||
|
||||
import { ToastsStore } from '../stores/toasts';
|
||||
import { RecipesStore } from '../stores/recipes';
|
||||
import { IngredientsStore, updateIngredients } from '../stores/ingredients';
|
||||
updateIngredients();
|
||||
|
||||
|
@ -86,6 +88,7 @@
|
|||
|
||||
response.then((response) => {
|
||||
if (response.status === 200) {
|
||||
RecipesStore.dismiss();
|
||||
if (!recipe.id) {
|
||||
response.json().then((data) => {
|
||||
recipe = data;
|
||||
|
@ -95,7 +98,11 @@
|
|||
edit = false;
|
||||
} else {
|
||||
disabled = false;
|
||||
console.log(response);
|
||||
response.json().then((data) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: data.errmsg,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -106,9 +113,14 @@
|
|||
headers: {'Accept': 'application/json'},
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
RecipesStore.dismiss();
|
||||
goto('recipes');
|
||||
} else {
|
||||
console.log(response);
|
||||
response.json().then((data) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: data.errmsg,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -276,14 +288,16 @@
|
|||
</ul>
|
||||
{/if}
|
||||
|
||||
<hr>
|
||||
{#if recipe.id}
|
||||
<hr>
|
||||
|
||||
<h3 style="font-size: 100%" class="text-muted">
|
||||
Photos
|
||||
</h3>
|
||||
<RecipePictures
|
||||
recipe={recipe}
|
||||
/>
|
||||
<h3 style="font-size: 100%" class="text-muted">
|
||||
Photos
|
||||
</h3>
|
||||
<RecipePictures
|
||||
recipe={recipe}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<hr>
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<div
|
||||
class="card card-recipe d-flex flex-column-reverse"
|
||||
style="border-radius: 15px; border: 1px solid #ffe8c0; background-image: url(img/kebab.webp); background-size: cover; background-position: center; cursor: pointer;"
|
||||
style="border-radius: 15px; border: 1px solid #ffe8c0; background-image: url({recipe.picture}); background-size: cover; background-position: center; cursor: pointer;"
|
||||
on:click={goToRecipe}
|
||||
>
|
||||
{#if recipe.title}
|
||||
|
@ -32,6 +32,6 @@
|
|||
|
||||
<style>
|
||||
.card-recipe {
|
||||
border-radius: 15px; border: 1px solid black; background-image: url(img/kebab.webp); background-size: cover; background-position: center; cursor: pointer;
|
||||
border-radius: 15px; border: 1px solid black; background-image: url({recipe.picture}); background-size: cover; background-position: center; cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -30,16 +30,18 @@
|
|||
{:else}
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center page-header"
|
||||
style="background-image: url(img/kebab.webp)"
|
||||
style="background-image: url({recipe.picture})"
|
||||
>
|
||||
<h1>
|
||||
{recipe.title}
|
||||
</h1>
|
||||
{#await GetUser(recipe.author_id) then author}
|
||||
<h2>
|
||||
Par {author.username}
|
||||
</h2>
|
||||
{/await}
|
||||
{#if recipe.author_id}
|
||||
{#await GetUser(recipe.author_id) then author}
|
||||
<h2>
|
||||
Par {author.username}
|
||||
</h2>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
InputGroup,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { ToastsStore } from '../stores/toasts';
|
||||
|
||||
export let recipe = null
|
||||
|
||||
let pictures = [];
|
||||
$: {
|
||||
async function getPictures() {
|
||||
if (recipe && recipe.id) {
|
||||
fetch('api/recipes/' + encodeURIComponent(recipe.id) + '/pictures', { headers: {'Accept': 'application/json'} }).then(
|
||||
(response) => {
|
||||
|
@ -20,19 +22,85 @@
|
|||
});
|
||||
}
|
||||
}
|
||||
getPictures()
|
||||
|
||||
let files = [];
|
||||
async function uploadFile() {
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append("recipepicture", file);
|
||||
formData.append("meta", JSON.stringify({
|
||||
label: "test",
|
||||
}));
|
||||
|
||||
const response = await fetch('/api/recipes/' + encodeURIComponent(recipe.id) + '/pictures', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
if (response.ok) {
|
||||
getPictures();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deletePicture(picture) {
|
||||
return async function() {
|
||||
const response = await fetch('/api/recipes/' + encodeURIComponent(recipe.id) + '/pictures/' + encodeURIComponent(picture.id), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.ok) {
|
||||
getPictures();
|
||||
} else {
|
||||
const err = await response.json();
|
||||
ToastsStore.addErrorToast({ msg: err.errmsg });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="d-inline-flex flex-wrap" style="gap: .5rem .5rem">
|
||||
{#if pictures}
|
||||
{#each pictures as picture,k (k)}
|
||||
<Card style="width: 46%">
|
||||
<Button
|
||||
variant="link"
|
||||
class="position-absolute"
|
||||
on:click={deletePicture(picture)}
|
||||
>
|
||||
<Icon name="x-circle-fill" />
|
||||
</Button>
|
||||
<img class="card-img-top" src={picture.url} alt="recipe picture" />
|
||||
</Card>
|
||||
{/each}
|
||||
{/if}
|
||||
<Input
|
||||
type="file"
|
||||
style="width: 46%"
|
||||
class="form-control-file"
|
||||
/>
|
||||
<div class="card picture-add-card">
|
||||
<input
|
||||
type="file"
|
||||
id="recipePictureFileInput"
|
||||
bind:files={files}
|
||||
on:change={uploadFile}
|
||||
>
|
||||
<label
|
||||
class="picture-add h-100 d-flex justify-content-center align-items-center"
|
||||
for="recipePictureFileInput"
|
||||
>
|
||||
Ajouter photo
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#recipePictureFileInput {
|
||||
display: none;
|
||||
}
|
||||
.picture-add-card {
|
||||
border: 2px dashed #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
.picture-add-card:hover {
|
||||
border-color: #666;
|
||||
}
|
||||
.picture-add-card label {
|
||||
padding: 3em 1em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,9 +5,36 @@
|
|||
Col,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { RecipesStore, updateRecipes } from '../stores/recipes';
|
||||
import RecipeCard from '../components/RecipeCard.svelte';
|
||||
|
||||
updateRecipes();
|
||||
|
||||
function shuffleArray(arr) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
}
|
||||
function lastestRecipes() {
|
||||
return $RecipesStore.list.slice($RecipesStore.list.length-5)
|
||||
}
|
||||
function randomRecipes() {
|
||||
let arr = []
|
||||
for (let i = $RecipesStore.list.length - 1; i > 0; i--) {
|
||||
arr.push(i);
|
||||
}
|
||||
shuffleArray(arr);
|
||||
|
||||
let ret = [];
|
||||
for (let i = Math.min(arr.length - 1, 5); i >= 0; i--) {
|
||||
ret.push($RecipesStore.list[arr[i]]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="h-100 d-flex justify-content-around flex-column">
|
||||
<Container>
|
||||
<Row>
|
||||
<Col sm={6} class="mb-5 mb-sm-0">
|
||||
|
@ -21,11 +48,11 @@
|
|||
</Col>
|
||||
</Row>
|
||||
<div style="overflow-x: scroll; gap: 1em 1em" class="d-flex flex-nowrap index-flex">
|
||||
<RecipeCard title="Kebab au mouton facile" />
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
{#if $RecipesStore.list.length}
|
||||
{#each lastestRecipes() as recipe}
|
||||
<RecipeCard {recipe} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Col>
|
||||
<Col sm={6}>
|
||||
|
@ -38,11 +65,11 @@
|
|||
</Col>
|
||||
</Row>
|
||||
<div style="overflow-x: scroll; gap: 1em 1em" class="d-flex flex-nowrap index-flex">
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
<RecipeCard />
|
||||
{#if $RecipesStore.list.length}
|
||||
{#each randomRecipes() as recipe}
|
||||
<RecipeCard {recipe} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -74,6 +101,7 @@
|
|||
<RecipeCard />
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.index-flex > *) {
|
||||
|
|
|
@ -13,12 +13,14 @@
|
|||
|
||||
<Container fluid class="flex-fill py-3">
|
||||
<Row cols={{ xs: 2, sm: 3, md: 4, lg: 5, xl: 6 }} style="gap: 1rem 0">
|
||||
{#each $RecipesStore.list as recipe}
|
||||
<Col>
|
||||
<div class="ratio ratio-1x1">
|
||||
<RecipeCard {recipe} />
|
||||
</div>
|
||||
{#if $RecipesStore.list.length}
|
||||
{#each $RecipesStore.list as recipe}
|
||||
<Col>
|
||||
<div class="ratio ratio-1x1">
|
||||
<RecipeCard {recipe} />
|
||||
</div>
|
||||
</Col>
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</Row>
|
||||
</Container>
|
||||
|
|
|
@ -9,7 +9,7 @@ function createRecipesStore() {
|
|||
const { subscribe, set, update } = writable({list: [], recipes: {}, recipes_idx: {}, cache_expires: null});
|
||||
|
||||
const dismiss = () => {
|
||||
update((i) => ({recipes: [], recipes_idx: {}, cache_expires: null}));
|
||||
update((i) => ({list: [], recipes: {}, recipes_idx: {}, cache_expires: null}));
|
||||
}
|
||||
|
||||
const getRecipe = async (rid) => {
|
||||
|
@ -18,6 +18,9 @@ function createRecipesStore() {
|
|||
});
|
||||
if (res_recipe.status === 200) {
|
||||
res_recipe.json().then((recipe) => {
|
||||
if (!recipe.picture) {
|
||||
recipe.picture = 'img/default-recipe.jpg';
|
||||
}
|
||||
update((i) => {
|
||||
i.recipes[rid] = recipe;
|
||||
return i;
|
||||
|
@ -43,6 +46,9 @@ function createRecipesStore() {
|
|||
res_recipes.json().then((recipes) => {
|
||||
const recipes_idx = {};
|
||||
recipes.forEach(function(recipe, k) {
|
||||
if (!recipe.picture) {
|
||||
recipe.picture = 'img/default-recipe.jpg';
|
||||
}
|
||||
recipes_idx[recipe.id] = recipe;
|
||||
})
|
||||
update((i) => (Object.assign(i, {list: recipes, recipes_idx, cache_expires: new Date() + 300000})));
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
Loading…
Reference in New Issue