CRUD for project
This commit is contained in:
File diff suppressed because one or more lines are too long
36
assets/views/website/create-project.html
Normal file
36
assets/views/website/create-project.html
Normal 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%}
|
202
assets/views/website/macros/project-item.html
Normal file
202
assets/views/website/macros/project-item.html
Normal 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 %}
|
@@ -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 %}
|
|
@@ -36,8 +36,9 @@
|
|||||||
{% if project.github_url %}
|
{% if project.github_url %}
|
||||||
<a href="{{ project.github_url }}" target="_blank" rel="noopener noreferrer"
|
<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">
|
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"
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="lucide lucide-github">
|
||||||
<path
|
<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" />
|
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" />
|
<path d="M9 18c-4.51 2-5-2-7-2" />
|
||||||
@@ -47,9 +48,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if project.visit_url %}
|
{% if project.visit_url %}
|
||||||
<a href="{{ project.visit_url }}" target="_blank" rel="noopener noreferrer"
|
<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">
|
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"
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye">
|
||||||
<path
|
<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" />
|
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" />
|
<circle cx="12" cy="12" r="3" />
|
||||||
@@ -59,9 +61,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if project.download_url %}
|
{% if project.download_url %}
|
||||||
<a href="{{ project.download_url }}" target="_blank" rel="noopener noreferrer"
|
<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">
|
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"
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-download">
|
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 13v8l-4-4" />
|
||||||
<path d="m12 21 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" />
|
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />
|
@@ -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 %}
|
{% extends "website/base.html" %} {% block content %}
|
||||||
<span class="m-8"></span>
|
<span class="m-8"></span>
|
||||||
<div class="flex flex-col w-full gap-4 m-4">
|
<div class="flex flex-col w-full gap-4 m-4">
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
use core::task;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -49,6 +48,7 @@ impl Hooks for App {
|
|||||||
|
|
||||||
fn routes(_ctx: &AppContext) -> AppRoutes {
|
fn routes(_ctx: &AppContext) -> AppRoutes {
|
||||||
AppRoutes::with_default_routes() // controller routes below
|
AppRoutes::with_default_routes() // controller routes below
|
||||||
|
.add_route(controllers::project::routes())
|
||||||
.add_route(controllers::data::routes())
|
.add_route(controllers::data::routes())
|
||||||
.add_route(controllers::auth::routes())
|
.add_route(controllers::auth::routes())
|
||||||
.add_route(controllers::website::routes())
|
.add_route(controllers::website::routes())
|
||||||
@@ -76,6 +76,7 @@ impl Hooks for App {
|
|||||||
tasks.register(tasks::add_data_file::AddDataFile);
|
tasks.register(tasks::add_data_file::AddDataFile);
|
||||||
tasks.register(tasks::delete_data::DeleteData);
|
tasks.register(tasks::delete_data::DeleteData);
|
||||||
tasks.register(tasks::clear_data::ClearData);
|
tasks.register(tasks::clear_data::ClearData);
|
||||||
|
tasks.register(tasks::delete_project::DeleteProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn truncate(db: &DatabaseConnection) -> Result<()> {
|
async fn truncate(db: &DatabaseConnection) -> Result<()> {
|
||||||
|
@@ -2,3 +2,4 @@ pub mod auth;
|
|||||||
pub mod website;
|
pub mod website;
|
||||||
|
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
pub mod project;
|
46
src/controllers/project.rs
Normal file
46
src/controllers/project.rs
Normal 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))
|
||||||
|
}
|
@@ -35,7 +35,7 @@ pub async fn render_projects(
|
|||||||
ViewEngine(v): ViewEngine<TeraView>,
|
ViewEngine(v): ViewEngine<TeraView>,
|
||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
views::website::projects(v, &ctx).await
|
views::projects::projects(v, &ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_project_detail(
|
pub async fn render_project_detail(
|
||||||
@@ -43,7 +43,7 @@ pub async fn render_project_detail(
|
|||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
Path(id): Path<i32>,
|
Path(id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> 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(
|
pub async fn render_project_detail_from_name(
|
||||||
@@ -51,7 +51,13 @@ pub async fn render_project_detail_from_name(
|
|||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> 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(
|
pub async fn render_data(
|
||||||
@@ -73,6 +79,7 @@ pub fn routes() -> Routes {
|
|||||||
.add("/upload", get(render_upload))
|
.add("/upload", get(render_upload))
|
||||||
.add("/login", get(render_login))
|
.add("/login", get(render_login))
|
||||||
.add("/projects", get(render_projects))
|
.add("/projects", get(render_projects))
|
||||||
|
.add("/projects/create", get(render_create_project))
|
||||||
.add("/projects/:id", get(render_project_detail))
|
.add("/projects/:id", get(render_project_detail))
|
||||||
.add("/projects/project/:name", get(render_project_detail_from_name))
|
.add("/projects/project/:name", get(render_project_detail_from_name))
|
||||||
.add("/data", get(render_data))
|
.add("/data", get(render_data))
|
||||||
|
@@ -42,6 +42,30 @@ pub struct CreateProject {
|
|||||||
pub technologies: Vec<String>,
|
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 {
|
pub fn get_category_from_string(category: &str) -> Category {
|
||||||
match category {
|
match category {
|
||||||
"Web" => Category::Web,
|
"Web" => Category::Web,
|
||||||
|
@@ -226,3 +226,22 @@ pub async fn delete_multiple_data_by_file_names(
|
|||||||
}
|
}
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
@@ -6,23 +6,24 @@ use crate::{
|
|||||||
project_thumbnails,
|
project_thumbnails,
|
||||||
projects::{self, ActiveModel, Entity, Model},
|
projects::{self, ActiveModel, Entity, Model},
|
||||||
},
|
},
|
||||||
projects::{get_category_from_string, get_string_from_category, Category, CreateProject, ProjectDto},
|
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
|
||||||
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?;
|
let projects = Entity::find().all(&ctx.db).await?;
|
||||||
Ok(projects)
|
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 = Entity::find_by_id(id).one(&ctx.db).await?;
|
||||||
let project = project.ok_or_else(|| ModelError::EntityNotFound)?;
|
let project = project.ok_or_else(|| ModelError::EntityNotFound)?;
|
||||||
Ok(project)
|
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()
|
let project = Entity::find()
|
||||||
.filter(projects::Column::Name.contains(name))
|
.filter(projects::Column::Name.contains(name))
|
||||||
.one(&ctx.db)
|
.one(&ctx.db)
|
||||||
@@ -31,7 +32,7 @@ pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> Result<Model>
|
|||||||
Ok(project)
|
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()
|
let archived_projects = Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
model::query::condition()
|
model::query::condition()
|
||||||
@@ -43,7 +44,7 @@ pub async fn get_archived_projects(ctx: &AppContext) -> Result<Vec<Model>> {
|
|||||||
Ok(archived_projects)
|
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()
|
let highlighted_projects = Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
model::query::condition()
|
model::query::condition()
|
||||||
@@ -161,3 +162,124 @@ pub async fn add_project(
|
|||||||
|
|
||||||
Ok(item)
|
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(())
|
||||||
|
}
|
30
src/tasks/delete_project.rs
Normal file
30
src/tasks/delete_project.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@@ -5,3 +5,4 @@ pub mod create_user;
|
|||||||
pub mod seed;
|
pub mod seed;
|
||||||
pub mod clear_data;
|
pub mod clear_data;
|
||||||
pub mod delete_data;
|
pub mod delete_data;
|
||||||
|
pub mod delete_project;
|
@@ -1,3 +1,4 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod website;
|
pub mod website;
|
||||||
|
pub mod projects;
|
24
src/views/projects.rs
Normal file
24
src/views/projects.rs
Normal 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!({}))
|
||||||
|
}
|
@@ -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> {
|
pub async fn about(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||||
let age = services::website::get_current_age();
|
let age = services::website::get_current_age();
|
||||||
|
|
||||||
|
@@ -2,3 +2,4 @@ mod auth;
|
|||||||
mod prepare_data;
|
mod prepare_data;
|
||||||
|
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
pub mod project;
|
17
tests/requests/project.rs
Normal file
17
tests/requests/project.rs
Normal 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;
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user