CRUD for project

This commit is contained in:
2024-11-10 10:54:20 +01:00
parent 1ccded15cc
commit 41ad5ab612
21 changed files with 568 additions and 163 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
{% extends "website/base.html" %} {% block content %}
<div class="w-full mt-16"></div>
<form method="post" id="project-upload" class="flex flex-col gap-2 text-black" action="/api/projects">
<label class="text-white" for="name">Project Name:</label>
<input type="text" id="name" name="name" required />
<label class="text-white" for="short-description">Short Description:</label>
<input type="text" id="short-description" name="short_description" required />
<label class="text-white" for="description">Description:</label>
<textarea id="description" name="description"></textarea>
<label class="text-white" for="category">Category</label>
<select id="category" name="category" required>
<option value="Web">Web</option>
<option value="Mobile">Mobile</option>
<option value="Desktop">Desktop</option>
<option value="Game">Game</option>
<option value="Api">Api</option>
</select>
<label class="text-white" for="github">Github Repository:</label>
<input type="text" id="github" name="github_url" />
<label class="text-white" for="website">Website:</label>
<input type="text" id="website" name="website_url" />
<label class="text-white" for="download">Download:</label>
<input type="text" id="download" name="download_url" />
<label class="text-white" for="technology">Technologies:</label>
<input type="text" id="technology" name="technologies" required />
<button type="submit" class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600">Submit</button>
</form>
{% endblock content%}

View File

@@ -0,0 +1,202 @@
{% import "website/macros/chip.html" as chip %}
{% import "website/macros/image-carousel.html" as image_carousel %}
{% macro project_item(project) %}
<script src="/static/js/project-item.js"></script>
<div class="flex items-center justify-between w-full h-full gap-4 text-white">
<div class="flex flex-col w-full gap-4 m-4 md:w-1/3">
<div class="prose">
<h1 class="text-white">{{ project.name }}</h1>
<p class="text-white whitespace-pre-wrap">{{ project.short_description }}</p>
</div>
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
{% for technology in project.technologies %}
{{ chip::chip(text=technology) }}
{% endfor %}
</div>
<a href="/projects/project/{{ project.name }}"
class="w-full p-2 text-center border border-yellow-400 rounded-xl hover:bg-yellow-400">Read more</a>
<div class="flex flex-wrap gap-2 sm:justify-center md:justify-start">
{% if project.github_url %}
<a href="{{ project.github_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-github">
<path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
</svg>
CODE
</a>
{% endif %}
{% if project.visit_url %}
<a href="{{ project.visit_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-eye">
<path
d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>
LIVE
</a>
{% endif %}
{% if project.download_url %}
<a href="{{ project.download_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-cloud-download">
<path d="M12 13v8l-4-4" />
<path d="m12 21 4-4" />
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />
</svg>
DOWNLOAD
</a>
{% endif %}
</div>
{% if project.thumbnails|length > 0 %}
<div class="w-full m-2 md:hidden">
{% set carousel_id = "carousel-mobile-" ~ project.id %}
{{ image_carousel::image_carousel(id=carousel_id, thumbnails=project.thumbnails) }}
</div>
{% endif %}
{% if project.thumbnails|length == 0 %}
<div class="w-full m-2 md:hidden">
<div
class="bg-gradient-to-r from-violet-600 to-indigo-600 shadow-lg w-full max-w-full md:max-w-[50hw] h-[40rem] flex items-center justify-center">
{% if project.category == "Desktop" %}
<svg title="desktop" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-app-window">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M10 4v4" />
<path d="M2 8h20" />
<path d="M6 4v4" />
</svg>
{% endif %}
{% if project.category == "Mobile" %}
<svg title="mobile" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-smartphone">
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
<path d="M12 18h.01" />
</svg>
{%endif%}
{% if project.category == "Web" %}
<svg title="web" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-globe">
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>
{%endif%}
{% if project.category == "Api" %}
<svg title="API" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-monitor-cog">
<path d="M12 17v4" />
<path d="m15.2 4.9-.9-.4" />
<path d="m15.2 7.1-.9.4" />
<path d="m16.9 3.2-.4-.9" />
<path d="m16.9 8.8-.4.9" />
<path d="m19.5 2.3-.4.9" />
<path d="m19.5 9.7-.4-.9" />
<path d="m21.7 4.5-.9.4" />
<path d="m21.7 7.5-.9-.4" />
<path d="M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" />
<path d="M8 21h8" />
<circle cx="18" cy="6" r="3" />
</svg>
{%endif%}
{% if project.category == "Game" %}
<svg title="game" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-gamepad-2">
<line x1="6" x2="10" y1="11" y2="11" />
<line x1="8" x2="8" y1="9" y2="13" />
<line x1="15" x2="15.01" y1="12" y2="12" />
<line x1="18" x2="18.01" y1="10" y2="10" />
<path
d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z" />
</svg>
{%endif%}
</div>
</div>
{% endif %}
</div>
{% if project.thumbnails|length > 0 %}
<div class="hidden m-2 md:flex md:w-1/2">
{% set carousel_id = "carousel-desktop-" ~ project.id %}
{{ image_carousel::image_carousel(id=carousel_id, thumbnails=project.thumbnails) }}
</div>
{% endif %}
{% if project.thumbnails|length == 0 %}
<div class="hidden m-2 md:flex md:w-1/2">
<div
class="bg-gradient-to-r from-violet-600 to-indigo-600 shadow-lg w-full max-w-full md:max-w-[50hw] h-[40rem] flex items-center justify-center">
{% if project.category == "Desktop" %}
<svg title="desktop" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-app-window">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M10 4v4" />
<path d="M2 8h20" />
<path d="M6 4v4" />
</svg>
{% endif %}
{% if project.category == "Mobile" %}
<svg title="mobile" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-smartphone">
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
<path d="M12 18h.01" />
</svg>
{%endif%}
{% if project.category == "Web" %}
<svg title="web" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-globe">
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>
{%endif%}
{% if project.category == "Api" %}
<svg title="API" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-monitor-cog">
<path d="M12 17v4" />
<path d="m15.2 4.9-.9-.4" />
<path d="m15.2 7.1-.9.4" />
<path d="m16.9 3.2-.4-.9" />
<path d="m16.9 8.8-.4.9" />
<path d="m19.5 2.3-.4.9" />
<path d="m19.5 9.7-.4-.9" />
<path d="m21.7 4.5-.9.4" />
<path d="m21.7 7.5-.9-.4" />
<path d="M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" />
<path d="M8 21h8" />
<circle cx="18" cy="6" r="3" />
</svg>
{%endif%}
{% if project.category == "Game" %}
<svg title="game" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-gamepad-2">
<line x1="6" x2="10" y1="11" y2="11" />
<line x1="8" x2="8" y1="9" y2="13" />
<line x1="15" x2="15.01" y1="12" y2="12" />
<line x1="18" x2="18.01" y1="10" y2="10" />
<path
d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z" />
</svg>
{%endif%}
</div>
</div>
{% endif %}
</div>
{% endmacro project_item %}

View File

@@ -1,112 +0,0 @@
{% import "website/macros/chip.html" as chip %}
{% import "website/macros/image_carousel.html" as image_carousel %}
{% macro project_item(project) %}
<script src="/static/js/project-item.js"></script>
<div class="flex items-center justify-between w-full h-full gap-4 text-white">
<div class="flex flex-col w-full gap-4 m-4 md:w-1/3">
<div class="prose">
<h1 class="text-white">{{ project.name }}</h1>
<p class="text-white whitespace-pre-wrap">{{ project.short_description }}</p>
</div>
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
{% for technology in project.technologies %}
{{ chip::chip(text=technology) }}
{% endfor %}
</div>
<a href="/projects/project/{{ project.name }}"
class="w-full p-2 text-center border border-yellow-400 rounded-xl hover:bg-yellow-400">Read more</a>
<div class="flex flex-wrap gap-2 sm:justify-center md:justify-start">
{% if project.github_url %}
<a href="{{ project.github_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github">
<path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
</svg>
CODE
</a>
{% endif %}
{% if project.visit_url %}
<a href="{{ project.visit_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>
LIVE
</a>
{% endif %}
{% if project.download_url %}
<a href="{{ project.download_url }}" target="_blank" rel="noopener noreferrer"
class="flex justify-center gap-1 p-2 text-center border border-yellow-400 items-centerw-full rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-download">
<path d="M12 13v8l-4-4" />
<path d="m12 21 4-4" />
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />
</svg>
DOWNLOAD
</a>
{% endif %}
</div>
{% if project.thumbnails|length > 0 %}
<div class="w-full m-2 md:hidden">
{% set carousel_id = "carousel-mobile-" ~ project.id %}
{{ image_carousel::image_carousel(id=carousel_id, thumbnails=project.thumbnails) }}
</div>
{% endif %}
{% if project.thumbnails|length == 0 %}
<div class="w-full m-2 md:hidden">
<div class="bg-gradient-to-r from-violet-600 to-indigo-600 shadow-lg w-full max-w-full md:max-w-[50hw] h-[40rem] flex items-center justify-center">
{% if project.category == "Desktop" %}
<svg title="desktop" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-app-window"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M10 4v4"/><path d="M2 8h20"/><path d="M6 4v4"/></svg>
{% endif %}
{% if project.category == "Mobile" %}
<svg title="mobile" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>
{%endif%}
{% if project.category == "Web" %}
<svg title="web" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
{%endif%}
{% if project.category == "Api" %}
<svg title="API" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-monitor-cog"><path d="M12 17v4"/><path d="m15.2 4.9-.9-.4"/><path d="m15.2 7.1-.9.4"/><path d="m16.9 3.2-.4-.9"/><path d="m16.9 8.8-.4.9"/><path d="m19.5 2.3-.4.9"/><path d="m19.5 9.7-.4-.9"/><path d="m21.7 4.5-.9.4"/><path d="m21.7 7.5-.9-.4"/><path d="M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M8 21h8"/><circle cx="18" cy="6" r="3"/></svg>
{%endif%}
{% if project.category == "Game" %}
<svg title="game" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-gamepad-2"><line x1="6" x2="10" y1="11" y2="11"/><line x1="8" x2="8" y1="9" y2="13"/><line x1="15" x2="15.01" y1="12" y2="12"/><line x1="18" x2="18.01" y1="10" y2="10"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"/></svg>
{%endif%}
</div>
</div>
{% endif %}
</div>
{% if project.thumbnails|length > 0 %}
<div class="hidden m-2 md:flex md:w-1/2">
{% set carousel_id = "carousel-desktop-" ~ project.id %}
{{ image_carousel::image_carousel(id=carousel_id, thumbnails=project.thumbnails) }}
</div>
{% endif %}
{% if project.thumbnails|length == 0 %}
<div class="hidden m-2 md:flex md:w-1/2">
<div class="bg-gradient-to-r from-violet-600 to-indigo-600 shadow-lg w-full max-w-full md:max-w-[50hw] h-[40rem] flex items-center justify-center">
{% if project.category == "Desktop" %}
<svg title="desktop" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-app-window"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M10 4v4"/><path d="M2 8h20"/><path d="M6 4v4"/></svg>
{% endif %}
{% if project.category == "Mobile" %}
<svg title="mobile" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>
{%endif%}
{% if project.category == "Web" %}
<svg title="web" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
{%endif%}
{% if project.category == "Api" %}
<svg title="API" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-monitor-cog"><path d="M12 17v4"/><path d="m15.2 4.9-.9-.4"/><path d="m15.2 7.1-.9.4"/><path d="m16.9 3.2-.4-.9"/><path d="m16.9 8.8-.4.9"/><path d="m19.5 2.3-.4.9"/><path d="m19.5 9.7-.4-.9"/><path d="m21.7 4.5-.9.4"/><path d="m21.7 7.5-.9-.4"/><path d="M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M8 21h8"/><circle cx="18" cy="6" r="3"/></svg>
{%endif%}
{% if project.category == "Game" %}
<svg title="game" xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-gamepad-2"><line x1="6" x2="10" y1="11" y2="11"/><line x1="8" x2="8" y1="9" y2="13"/><line x1="15" x2="15.01" y1="12" y2="12"/><line x1="18" x2="18.01" y1="10" y2="10"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"/></svg>
{%endif%}
</div>
</div>
{% endif %}
</div>
{% endmacro project_item %}

View File

@@ -36,8 +36,9 @@
{% if project.github_url %}
<a href="{{ project.github_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-github">
<path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
@@ -47,9 +48,10 @@
{% endif %}
{% if project.visit_url %}
<a href="{{ project.visit_url }}" target="_blank" rel="noopener noreferrer"
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye">
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-eye">
<path
d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
@@ -59,9 +61,10 @@
{% endif %}
{% if project.download_url %}
<a href="{{ project.download_url }}" target="_blank" rel="noopener noreferrer"
class="flex justify-center gap-1 p-2 text-center border border-yellow-400 items-centerw-full rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-download">
class="flex items-center justify-center w-full gap-1 p-2 text-center border border-yellow-400 jus rounded-xl hover:bg-yellow-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-cloud-download">
<path d="M12 13v8l-4-4" />
<path d="m12 21 4-4" />
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />

View File

@@ -1,4 +1,4 @@
{% import "website/macros/project_item.html" as project_item %}
{% import "website/macros/project-item.html" as project_item %}
{% extends "website/base.html" %} {% block content %}
<span class="m-8"></span>
<div class="flex flex-col w-full gap-4 m-4">

View File

@@ -1,4 +1,3 @@
use core::task;
use std::path::Path;
use async_trait::async_trait;
@@ -49,6 +48,7 @@ impl Hooks for App {
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes() // controller routes below
.add_route(controllers::project::routes())
.add_route(controllers::data::routes())
.add_route(controllers::auth::routes())
.add_route(controllers::website::routes())
@@ -76,6 +76,7 @@ impl Hooks for App {
tasks.register(tasks::add_data_file::AddDataFile);
tasks.register(tasks::delete_data::DeleteData);
tasks.register(tasks::clear_data::ClearData);
tasks.register(tasks::delete_project::DeleteProject);
}
async fn truncate(db: &DatabaseConnection) -> Result<()> {

View File

@@ -2,3 +2,4 @@ pub mod auth;
pub mod website;
pub mod data;
pub mod project;

View File

@@ -0,0 +1,46 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use crate::{
models::{
projects::{get_category_from_string, CreateProject, CreateProjectForm},
users,
},
services,
shared::get_technologies_from_string::get_technologies_from_string,
};
async fn create_project(
auth: auth::JWT,
State(ctx): State<AppContext>,
Form(project_data): Form<CreateProjectForm>,
) -> Result<Response> {
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
Ok(_) => {}
Err(_) => return unauthorized("Unauthorized"),
}
let technologies = get_technologies_from_string(&project_data.technologies);
let project_data = CreateProject {
name: project_data.name,
description: project_data.description,
technologies,
category: get_category_from_string(&project_data.category),
download_url: project_data.download_url,
github_url: project_data.github_url,
visit_url: project_data.visit_url,
short_description: project_data.short_description,
};
let project = services::projects::add_project(&ctx, project_data).await?;
format::json(&project)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("api/projects/")
.add("/", post(create_project))
}

View File

@@ -35,7 +35,7 @@ pub async fn render_projects(
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
views::website::projects(v, &ctx).await
views::projects::projects(v, &ctx).await
}
pub async fn render_project_detail(
@@ -43,7 +43,7 @@ pub async fn render_project_detail(
State(ctx): State<AppContext>,
Path(id): Path<i32>,
) -> Result<impl IntoResponse> {
views::website::project_detail(v, &ctx, id).await
views::projects::project_detail(v, &ctx, id).await
}
pub async fn render_project_detail_from_name(
@@ -51,7 +51,13 @@ pub async fn render_project_detail_from_name(
State(ctx): State<AppContext>,
Path(name): Path<String>,
) -> Result<impl IntoResponse> {
views::website::project_detail_from_name(v, &ctx, name).await
views::projects::project_detail_from_name(v, &ctx, name).await
}
pub async fn render_create_project(
ViewEngine(v): ViewEngine<TeraView>,
) -> Result<impl IntoResponse> {
views::projects::create_project(v).await
}
pub async fn render_data(
@@ -73,6 +79,7 @@ pub fn routes() -> Routes {
.add("/upload", get(render_upload))
.add("/login", get(render_login))
.add("/projects", get(render_projects))
.add("/projects/create", get(render_create_project))
.add("/projects/:id", get(render_project_detail))
.add("/projects/project/:name", get(render_project_detail_from_name))
.add("/data", get(render_data))

View File

@@ -42,6 +42,30 @@ pub struct CreateProject {
pub technologies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateProjectForm {
pub name: String,
pub short_description: String,
pub description: Option<String>,
pub category: String,
pub github_url: Option<String>,
pub download_url: Option<String>,
pub visit_url: Option<String>,
pub technologies: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateProject {
pub name: Option<String>,
pub short_description: Option<String>,
pub description: Option<String>,
pub category: Option<Category>,
pub github_url: Option<String>,
pub download_url: Option<String>,
pub visit_url: Option<String>,
pub technologies: Option<Vec<String>>,
}
pub fn get_category_from_string(category: &str) -> Category {
match category {
"Web" => Category::Web,

View File

@@ -226,3 +226,22 @@ pub async fn delete_multiple_data_by_file_names(
}
Ok(())
}
pub async fn delete_data_by_id(id: i32, ctx: &AppContext) -> ModelResult<()> {
let data = Entity::find().filter(data::Column::Id.eq(id)).one(&ctx.db).await?;
match data {
Some(data) => {
let path = PathBuf::from(&data.file_url);
match ctx.storage.as_ref().delete(&path).await {
Ok(_) => {}
Err(_) => return Err(ModelError::Any("Failed to delete file from storage".into())),
}
data.delete(&ctx.db).await?;
}
None => return Err(ModelError::EntityNotFound),
}
Ok(())
}

View File

@@ -6,23 +6,24 @@ use crate::{
project_thumbnails,
projects::{self, ActiveModel, Entity, Model},
},
projects::{get_category_from_string, get_string_from_category, Category, CreateProject, ProjectDto},
},
shared::get_technologies_from_string::get_technologies_from_string,
projects::{get_category_from_string, get_string_from_category, CreateProject, ProjectDto, UpdateProject},
}, services::data::add_data_file_from_path, shared::get_technologies_from_string::get_technologies_from_string
};
pub async fn get_all_projects(ctx: &AppContext) -> Result<Vec<Model>> {
use super::data::delete_data_by_id;
pub async fn get_all_projects(ctx: &AppContext) -> ModelResult<Vec<Model>> {
let projects = Entity::find().all(&ctx.db).await?;
Ok(projects)
}
pub async fn get_project_by_id(ctx: &AppContext, id: i32) -> Result<Model> {
pub async fn get_project_by_id(ctx: &AppContext, id: i32) -> ModelResult<Model> {
let project = Entity::find_by_id(id).one(&ctx.db).await?;
let project = project.ok_or_else(|| ModelError::EntityNotFound)?;
Ok(project)
}
pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> Result<Model> {
pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> ModelResult<Model> {
let project = Entity::find()
.filter(projects::Column::Name.contains(name))
.one(&ctx.db)
@@ -31,7 +32,7 @@ pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> Result<Model>
Ok(project)
}
pub async fn get_archived_projects(ctx: &AppContext) -> Result<Vec<Model>> {
pub async fn get_archived_projects(ctx: &AppContext) -> ModelResult<Vec<Model>> {
let archived_projects = Entity::find()
.filter(
model::query::condition()
@@ -43,7 +44,7 @@ pub async fn get_archived_projects(ctx: &AppContext) -> Result<Vec<Model>> {
Ok(archived_projects)
}
pub async fn get_highlighted_projects(ctx: &AppContext) -> Result<Vec<Model>> {
pub async fn get_highlighted_projects(ctx: &AppContext) -> ModelResult<Vec<Model>> {
let highlighted_projects = Entity::find()
.filter(
model::query::condition()
@@ -161,3 +162,124 @@ pub async fn add_project(
Ok(item)
}
pub async fn add_project_with_thumbnails(
ctx: &AppContext,
thumbnails_paths: Vec<String>,
data: CreateProject,
) -> Result<()> {
let txn = ctx.db.begin().await?;
let project = add_project(ctx, data).await?;
let project_id = project.id;
for thumbnail_path in thumbnails_paths {
let thumbnail_data = add_data_file_from_path(ctx, &thumbnail_path, "thumbnail.png", false, true).await?;
let thumbnail = project_thumbnails::ActiveModel {
project_id: Set(project_id),
data_id: Set(thumbnail_data.id),
..Default::default()
};
thumbnail.insert(&txn).await?;
}
txn.commit().await?;
Ok(())
}
pub async fn update_project(
ctx: &AppContext,
id: i32,
data: UpdateProject,
) -> ModelResult<Model> {
let item = get_project_by_id(ctx, id).await?;
let mut item = item.into_active_model();
if let Some(name) = data.name {
item.name = Set(name);
}
if let Some(short_description) = data.short_description {
item.short_description = Set(short_description);
}
item.description = Set(data.description);
if let Some(category) = data.category {
item.category = Set(get_string_from_category(&category));
}
item.github_url = Set(data.github_url);
item.download_url = Set(data.download_url);
item.visit_url = Set(data.visit_url);
if let Some(technologies) = data.technologies {
item.technology = Set(technologies.join(","));
}
let item = item.update(&ctx.db).await?;
Ok(item)
}
pub async fn update_thumbnails_for_project(
ctx: &AppContext,
id: i32,
thumbnails_paths: Vec<String>,
) -> Result<()> {
let txn = ctx.db.begin().await?;
let project = get_project_by_id(ctx, id).await?;
let project_id = project.id;
let thumbnails = project
.find_related(project_thumbnails::Entity)
.all(&ctx.db)
.await?;
for thumbnail in thumbnails {
let _ = delete_data_by_id(thumbnail.data_id, ctx);
let _ = thumbnail.delete(&txn).await?;
}
for thumbnail_path in thumbnails_paths {
let thumbnail_data = add_data_file_from_path(ctx, &thumbnail_path, "thumbnail.png", false, true).await?;
let thumbnail = project_thumbnails::ActiveModel {
project_id: Set(project_id),
data_id: Set(thumbnail_data.id),
..Default::default()
};
thumbnail.insert(&txn).await?;
}
txn.commit().await?;
Ok(())
}
pub async fn delete_project(ctx: &AppContext, id: i32) -> Result<()> {
let item = get_project_by_id(ctx, id).await?;
let thumbnails = item.find_related(project_thumbnails::Entity).all(&ctx.db).await?;
let thumbnails_data_ids = thumbnails.into_iter().map(|thumbnail| thumbnail.data_id).collect::<Vec<i32>>();
for data_id in thumbnails_data_ids {
let _ = delete_data_by_id(data_id, ctx);
}
let _ = item.delete(&ctx.db).await?;
Ok(())
}
pub async fn delete_thumbnails_for_project(ctx: &AppContext, id: i32) -> Result<()> {
let project = get_project_by_id(ctx, id).await?;
let thumbnails = project
.find_related(project_thumbnails::Entity)
.all(&ctx.db)
.await?;
for thumbnail in thumbnails {
let _ = delete_data_by_id(thumbnail.data_id, ctx);
let _ = thumbnail.delete(&ctx.db).await?;
}
Ok(())
}

View File

@@ -0,0 +1,30 @@
use loco_rs::prelude::*;
use crate::services::projects;
pub struct DeleteProject;
#[async_trait]
impl Task for DeleteProject {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "delete_project".to_string(),
detail: "Task for deleting a project".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
let project_id = vars.cli_arg("id")?;
let project_id = project_id.parse::<i32>();
let project_id = match project_id {
Ok(project_id) => project_id,
Err(_) => return Err(Error::Any("Invalid project ID".into())),
};
projects::delete_project(app_context, project_id).await?;
tracing::info!("Project {} deleted successfully", project_id);
Ok(())
}
}

View File

@@ -5,3 +5,4 @@ pub mod create_user;
pub mod seed;
pub mod clear_data;
pub mod delete_data;
pub mod delete_project;

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod data;
pub mod website;
pub mod projects;

24
src/views/projects.rs Normal file
View File

@@ -0,0 +1,24 @@
use loco_rs::prelude::*;
use crate::services;
pub async fn projects(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoResponse> {
let projects = services::projects::get_all_projects_dto(ctx).await?;
format::render().view(&v, "website/projects.html", data!({"projects": projects}))
}
pub async fn project_detail(v: impl ViewRenderer, ctx: &AppContext, id: i32) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto(ctx, id).await?;
format::render().view(&v, "website/project-detail.html", data!({"project": project}))
}
pub async fn project_detail_from_name(v: impl ViewRenderer, ctx: &AppContext, name: String) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto_by_name(ctx, &name).await?;
format::render().view(&v, "website/project-detail.html", data!({"project": project}))
}
pub async fn create_project(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::render().view(&v, "website/create-project.html", data!({}))
}

View File

@@ -13,24 +13,6 @@ pub async fn index(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoRe
)
}
pub async fn projects(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoResponse> {
let projects = services::projects::get_all_projects_dto(ctx).await?;
format::render().view(&v, "website/projects.html", data!({"projects": projects}))
}
pub async fn project_detail(v: impl ViewRenderer, ctx: &AppContext, id: i32) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto(ctx, id).await?;
format::render().view(&v, "website/project_detail.html", data!({"project": project}))
}
pub async fn project_detail_from_name(v: impl ViewRenderer, ctx: &AppContext, name: String) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto_by_name(ctx, &name).await?;
format::render().view(&v, "website/project_detail.html", data!({"project": project}))
}
pub async fn about(v: impl ViewRenderer) -> Result<impl IntoResponse> {
let age = services::website::get_current_age();

View File

@@ -2,3 +2,4 @@ mod auth;
mod prepare_data;
pub mod data;
pub mod project;

17
tests/requests/project.rs Normal file
View File

@@ -0,0 +1,17 @@
use gabrielkaszewski_rs::app::App;
use loco_rs::testing;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn can_get_projects() {
testing::request::<App, _, _>(|request, _ctx| async move {
let res = request.get("/projects/").await;
assert_eq!(res.status_code(), 200);
// you can assert content like this:
// assert_eq!(res.text(), "content");
})
.await;
}