Compare commits

..

10 Commits

30 changed files with 568 additions and 162 deletions

View File

@@ -1,5 +1,4 @@
target target
dockerfile
.dockerignore .dockerignore
.git .git
.gitignore .gitignore

3
.gitignore vendored
View File

@@ -18,6 +18,9 @@ node_modules/
*.pdb *.pdb
*.sqlite *.sqlite
*.db
uploads/ uploads/
database/
assets/static/css/main.css assets/static/css/main.css
.env

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
const deleteForms = document.querySelectorAll('.delete-form');
const deleteData = async (form) => {
const fileIdInput = form.querySelector('.file-id');
const fileId = fileIdInput.value;
if (!fileId) {
console.warn('No file selected');
return;
}
try {
const response = await fetch(`/api/data/id/${fileId}`, {
method: 'DELETE',
});
if (response.ok) {
alert('Data deleted successfully');
window.location.reload();
fileIdInput.value = '';
} else {
console.error(
'Failed to delete data ',
response.status,
response.statusText
);
}
} catch (error) {
console.error('Error deleting data ', error);
}
};
deleteForms.forEach((form) => {
const deleteButton = form.querySelector('.delete-button');
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
deleteData(form);
});
});

View File

@@ -1,6 +1,7 @@
const form = document.getElementById('data-upload'); const form = document.getElementById('data-upload');
const fileInput = document.getElementById('file-input'); const fileInput = document.getElementById('file-input');
const protectedInput = document.getElementById('protected-input'); const protectedInput = document.getElementById('protected');
const uniqueNameInput = document.getElementById('unique_name');
const uploadData = async () => { const uploadData = async () => {
if (!fileInput.files.length) { if (!fileInput.files.length) {
@@ -11,6 +12,7 @@ const uploadData = async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', fileInput.files[0]); formData.append('file', fileInput.files[0]);
formData.append('protected', protectedInput.checked ? 'true' : 'false'); formData.append('protected', protectedInput.checked ? 'true' : 'false');
formData.append('unique_name', uniqueNameInput.checked ? 'true' : 'false');
try { try {
const response = await fetch('/api/data/upload', { const response = await fetch('/api/data/upload', {

View File

@@ -18,15 +18,15 @@
class="flex flex-col m-4 prose text-white md:text-left md:m-0 md:w-1/2 md:prose-lg lg:prose-xl prose-blue" class="flex flex-col m-4 prose text-white md:text-left md:m-0 md:w-1/2 md:prose-lg lg:prose-xl prose-blue"
> >
<p> <p>
Hi! I am Gabriel and I am {{ age }} years old. I study Bioinformatics at the Hi! I am Gabriel and I am {{ age }} years old. I graduated in Bioinformatics
University of Gdansk 🏫. I'm fluent in Polish and English and currently work from the University of Gdansk 🏫. I'm fluent in Polish and English and
as a Python Developer at digimonkeys.com 🐒💻. currently work as a Python Developer at digimonkeys.com 🐒💻.
</p> </p>
<p> <p>
I have published one article you can read it I have published one article you can read it
<a target="_blank" href="http://dx.doi.org/10.1038/s41598-023-44488-7"> <a target="_blank" href="http://dx.doi.org/10.1038/s41598-023-44488-7">
here here </a
</a>. >.
</p> </p>
</div> </div>
<h1 class="mt-6 text-3xl font-extrabold">Hobbies 🎮🎸</h1> <h1 class="mt-6 text-3xl font-extrabold">Hobbies 🎮🎸</h1>

View File

@@ -7,6 +7,8 @@
<title>Gabriel Kaszewski</title> <title>Gabriel Kaszewski</title>
<link rel="icon" href="/static/images/favicon.webp" type="image/x-icon" /> <link rel="icon" href="/static/images/favicon.webp" type="image/x-icon" />
<link rel="stylesheet" href="/static/css/main.css" /> <link rel="stylesheet" href="/static/css/main.css" />
<script defer data-domain="gabrielkaszewski.dev"
src="https://plausible.gabrielkaszewski.dev/js/script.file-downloads.outbound-links.js"></script>
</head> </head>
<body class="bg-gray-800 scroll-smooth"> <body class="bg-gray-800 scroll-smooth">

View File

@@ -0,0 +1,18 @@
{% extends "website/base.html" %} {% block content %}
<div class="w-full mt-16"></div>
<form method="post" class="flex flex-col gap-2 text-black" action="/api/jobs">
<label for="position">Position</label>
<input type="text" name="position" id="position" class="p-2 border border-gray-300 rounded-md" required>
<label for="company">Company</label>
<input type="text" name="company" id="company" class="p-2 border border-gray-300 rounded-md" required>
<label for="technologies">Technologies</label>
<input type="text" name="technologies" id="technologies" class="p-2 border border-gray-300 rounded-md" required>
<label for="start_date">Start date</label>
<input type="date" name="start_date" id="start_date" class="p-2 border border-gray-300 rounded-md" required>
<label for="end_date">End date</label>
<input type="date" name="end_date" id="end_date" class="p-2 border border-gray-300 rounded-md">
<label for="still_working">Still working</label>
<input type="checkbox" name="still_working" id="still_working" value="true">
<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

@@ -6,7 +6,11 @@
<input id="file-input" type="file" id="file" name="file" required /> <input id="file-input" type="file" id="file" name="file" required />
<div> <div>
<label class="text-white" for="protected">Protected:</label> <label class="text-white" for="protected">Protected:</label>
<input id="protected-input" type="checkbox" id="protected" name="protected" /> <input type="checkbox" id="protected" name="protected" />
</div>
<div>
<label class="text-white" for="unique_name">Unique name:</label>
<input type="checkbox" id="unique_name" name="unique_name" />
</div> </div>
<button class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" type="submit">Upload</button> <button class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" type="submit">Upload</button>
</form> </form>

View File

@@ -1,4 +1,5 @@
{% extends "website/base.html" %} {% block content %} {% extends "website/base.html" %} {% block content %}
<script src="/static/js/data-delete.js" defer></script>
<span class="mt-8"></span> <span class="mt-8"></span>
<a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" href="/upload">Add new file</a> <a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" href="/upload">Add new file</a>
<table class="table-fixed"> <table class="table-fixed">
@@ -18,6 +19,12 @@
<td>{{ file.protected }}</td> <td>{{ file.protected }}</td>
<td><a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" <td><a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600"
href="/api/data/{{ file.file_name }}">Download</a></td> href="/api/data/{{ file.file_name }}">Download</a></td>
<td>
<form class="delete-form">
<input class="file-id" id="file_id" type="hidden" name="file_id" value="{{ file.id }}" />
<button class="p-2 text-gray-900 bg-red-500 rounded-sm shadow hover:bg-red-600 delete-button">Delete</button>
</form>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -3,57 +3,113 @@
<div class="w-full"> <div class="w-full">
<div class="w-full mt-16 md:hidden"></div> <div class="w-full mt-16 md:hidden"></div>
<div class="relative inline-block w-full min-w-full"> <div class="relative inline-block w-full min-w-full">
<img src="/static/images/optimized-75.webp" alt="Background" <img
class="hidden object-cover w-full max-h-full pointer-events-none md:block" /> src="/static/images/optimized-75.webp"
alt="Background"
class="hidden object-cover w-full max-h-full pointer-events-none md:block"
/>
<div <div
class="flex flex-col items-center justify-start w-full md:inset-0 md:absolute md:items-start md:p-16 lg:p-20"> class="flex flex-col items-center justify-start w-full md:inset-0 md:absolute md:items-start md:p-16 lg:p-20"
>
<div class="hidden md:block"> <div class="hidden md:block">
<h1 <h1
class="mb-4 text-2xl font-bold tracking-tight text-white md:text-4xl lg:text-6xl md:mb-0 -motion-translate-x-in-100 motion-translate-y-in-75"> class="mb-4 text-2xl font-bold tracking-tight text-white md:text-4xl lg:text-6xl md:mb-0 -motion-translate-x-in-100 motion-translate-y-in-75"
>
Gabriel Kaszewski Gabriel Kaszewski
</h1> </h1>
<h2 <h2
class="mt-8 text-lg font-light tracking-tight text-white md:text-xl lg:text-2xl md:mt-0 motion-preset-slide-right motion-duration-1000"> class="mt-8 text-lg font-light tracking-tight text-white md:text-xl lg:text-2xl md:mt-0 motion-preset-slide-right motion-duration-1000"
>
Full-Stack Developer Full-Stack Developer
</h2> </h2>
</div> </div>
<div class="md:hidden"> <div class="md:hidden">
<h1 class="text-2xl font-bold tracking-tight text-white motion-preset-slide-right motion-duration-1000"> <h1
class="text-2xl font-bold tracking-tight text-white motion-preset-slide-right motion-duration-1000"
>
Gabriel Kaszewski Gabriel Kaszewski
</h1> </h1>
<h2 class="text-lg font-light tracking-tight text-white motion-preset-slide-right motion-duration-1000"> <h2
class="text-lg font-light tracking-tight text-white motion-preset-slide-right motion-duration-1000"
>
Full-Stack Developer Full-Stack Developer
</h2> </h2>
</div> </div>
<div class="flex items-center gap-2 mt-4 motion-preset-slide-right motion-duration-2000 md:mt-0"> <div
class="flex items-center gap-2 mt-4 motion-preset-slide-right motion-duration-2000 md:mt-0"
>
<a href="/api/data/cv.pdf" title="My CV"> <a href="/api/data/cv.pdf" title="My CV">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 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"
>
<path d="M14 2v4a2 2 0 0 0 2 2h4" /> <path d="M14 2v4a2 2 0 0 0 2 2h4" />
<path d="M15 18a3 3 0 1 0-6 0" /> <path d="M15 18a3 3 0 1 0-6 0" />
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z" /> <path
d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"
/>
<circle cx="12" cy="13" r="2" /> <circle cx="12" cy="13" r="2" />
</svg> </svg>
</a> </a>
<a href="https://github.com/GKaszewski" title="GitHub"> <a href="https://github.com/GKaszewski" title="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 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"
>
<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" />
</svg> </svg>
</a> </a>
<a href="mailto: gabrielkaszewski@gmail.com" title="My email"> <a href="mailto: gabrielkaszewski@gmail.com" title="My email">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 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"
>
<circle cx="12" cy="12" r="4" /> <circle cx="12" cy="12" r="4" />
<path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8" /> <path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8" />
</svg> </svg>
</a> </a>
<a href="https://www.linkedin.com/in/gabriel-kaszewski-5344b3183" title="LinkedIn"> <a
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" href="https://www.linkedin.com/in/gabriel-kaszewski-5344b3183"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> title="LinkedIn"
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" /> >
<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"
>
<path
d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
/>
<rect width="4" height="12" x="2" y="9" /> <rect width="4" height="12" x="2" y="9" />
<circle cx="4" cy="4" r="2" /> <circle cx="4" cy="4" r="2" />
</svg> </svg>
@@ -63,62 +119,100 @@
<div class="absolute bottom-0 hidden p-2 text-sm md:block"> <div class="absolute bottom-0 hidden p-2 text-sm md:block">
<span class="flex gap-1"> <span class="flex gap-1">
Photo by Photo by
<a class="underline" <a
href="https://unsplash.com/@federize?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash"> class="underline"
href="https://unsplash.com/@federize?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash"
>
Federico Beccari Federico Beccari
</a> </a>
on on
<a class="underline" <a
href="https://unsplash.com/photos/red-moon-eGJg5iRGlg8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash"> class="underline"
href="https://unsplash.com/photos/red-moon-eGJg5iRGlg8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash"
>
Unsplash Unsplash
</a> </a>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div id="who-am-i" class="flex flex-col items-center justify-center gap-4 p-4 rounded md:w-fit"> <div
id="who-am-i"
class="flex flex-col items-center justify-center gap-4 p-4 rounded md:w-fit"
>
<h3 class="mt-4 mb-2 text-5xl font-bold tracking-tight">Who am I? 🤔</h3> <h3 class="mt-4 mb-2 text-5xl font-bold tracking-tight">Who am I? 🤔</h3>
<section class="prose text-white md:prose-lg lg:prose-xl"> <section class="prose text-white md:prose-lg lg:prose-xl">
<p class="motion-preset-pop motion-delay-75"> <p class="motion-preset-pop motion-delay-75">
Hi, my name is Gabriel Kaszewski, and I am a Bioinformatics student 🧬 and Hi, my name is Gabriel Kaszewski - I'm a Bioinformatics graduate 🧬 and a
self-taught full-stack developer 💻. self-taught full-stack developer 💻.
</p> </p>
<p class="motion-preset-pop motion-delay-100"> <p class="motion-preset-pop motion-delay-100">
My journey with programming started when I was 11 🚀. I love solving problems and creating software that My journey with programming started when I was 11 🚀. I love solving
resolves them 👨‍💻. problems and creating software that resolves them 👨‍💻.
</p> </p>
<p class="motion-preset-pop motion-delay-200"> <p class="motion-preset-pop motion-delay-200">
Currently, I am working as a Python Developer at digimonkeys.com 🐒. In my free time I like to read about Currently, I am working as a Python Developer at digimonkeys.com 🐒. In my
new technologies and work on my projects 📚. free time I like to read about new technologies and work on my projects
📚.
</p> </p>
</section> </section>
</div> </div>
<div id="skills" class="flex flex-col items-center justify-center gap-4 p-4 rounded md:w-fit"> <div
id="skills"
class="flex flex-col items-center justify-center gap-4 p-4 rounded md:w-fit"
>
<h3 class="mt-4 mb-2 text-5xl font-bold tracking-tight">Skills 🛠️</h3> <h3 class="mt-4 mb-2 text-5xl font-bold tracking-tight">Skills 🛠️</h3>
<div class="flex flex-wrap justify-center w-1/2 gap-4"> <div class="flex flex-wrap justify-center w-1/2 gap-4">
{% for skill in skills %} <div {% for skill in skills %}
class="odd:motion-preset-slide-left even:motion-preset-slide-right odd:motion-delay-100"> <div
class="odd:motion-preset-slide-left even:motion-preset-slide-right odd:motion-delay-100"
>
{{ chip::chip(text=skill.name) }} {{ chip::chip(text=skill.name) }}
</div> </div>
{%endfor%} {%endfor%}
</div> </div>
</div> </div>
<div id="experience" class="flex flex-col items-center justify-center gap-4 p-4 rounded md:w-fit"> <div
id="experience"
class="flex flex-col items-center justify-center gap-4 p-4 rounded md:w-fit"
>
<h3 class="mt-4 mb-2 text-5xl font-bold tracking-tight">Experience 📈</h3> <h3 class="mt-4 mb-2 text-5xl font-bold tracking-tight">Experience 📈</h3>
{% for job in jobs %} {% for job in jobs %}
<div <div
class="flex flex-col gap-2 p-4 text-black bg-gray-50 rounded-lg w-[20rem] max-w-[20rem] shadow-lg odd:motion-preset-slide-right-md odd:motion-delay-100 even:motion-preset-rebound"> class="flex flex-col gap-2 p-4 text-black bg-gray-50 rounded-lg w-[20rem] max-w-[20rem] shadow-lg odd:motion-preset-slide-right-md odd:motion-delay-100 even:motion-preset-rebound"
>
<h4 class="flex items-center gap-1 text-2xl"> <h4 class="flex items-center gap-1 text-2xl">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-round"> 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-circle-user-round"
>
<path d="M18 20a6 6 0 0 0-12 0" /> <path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" /> <circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
</svg> {{ job.position }} </svg>
{{ job.position }}
</h4> </h4>
<h5 class="flex items-center gap-1 text-xl font-light"> <h5 class="flex items-center gap-1 text-xl font-light">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-building"> 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-building"
>
<rect width="16" height="20" x="4" y="2" rx="2" ry="2" /> <rect width="16" height="20" x="4" y="2" rx="2" ry="2" />
<path d="M9 22v-4h6v4" /> <path d="M9 22v-4h6v4" />
<path d="M8 6h.01" /> <path d="M8 6h.01" />
@@ -130,30 +224,62 @@
<path d="M16 14h.01" /> <path d="M16 14h.01" />
<path d="M8 10h.01" /> <path d="M8 10h.01" />
<path d="M8 14h.01" /> <path d="M8 14h.01" />
</svg> {{ job.company }} </svg>
{{ job.company }}
</h5> </h5>
{% if job.still_working %} {% if job.still_working %}
<h6 class="flex items-center gap-1"> <h6 class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock"> 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-clock"
>
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" /> <polyline points="12 6 12 12 16 14" />
</svg> {{ job.start_date | </svg>
date(format="%d-%m-%Y") }} - Present {{ job.start_date | date(format="%d-%m-%Y") }} - Present
</h6> </h6>
{% else %} {% else %}
<h6 class="flex items-center gap-1"> <h6 class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock"> 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-clock"
>
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" /> <polyline points="12 6 12 12 16 14" />
</svg> {{ job.start_date | </svg>
date(format="%d-%m-%Y") }} - {{ job.end_date | date(format="%d-%m-%Y") }} {{ job.start_date | date(format="%d-%m-%Y") }} - {{ job.end_date |
date(format="%d-%m-%Y") }}
</h6> </h6>
{% endif %} {% endif %}
<p class="flex items-center gap-1 font-bold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" <p class="flex items-center gap-1 font-bold">
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" <svg
stroke-linejoin="round" class="lucide lucide-microchip"> 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-microchip"
>
<path d="M18 12h2" /> <path d="M18 12h2" />
<path d="M18 16h2" /> <path d="M18 16h2" />
<path d="M18 20h2" /> <path d="M18 20h2" />
@@ -165,11 +291,14 @@
<path d="M4 4h2" /> <path d="M4 4h2" />
<path d="M4 8h2" /> <path d="M4 8h2" />
<path <path
d="M8 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-1.5c-.276 0-.494.227-.562.495a2 2 0 0 1-3.876 0C9.994 2.227 9.776 2 9.5 2z" /> d="M8 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-1.5c-.276 0-.494.227-.562.495a2 2 0 0 1-3.876 0C9.994 2.227 9.776 2 9.5 2z"
</svg> Technologies</p> />
</svg>
Technologies
</p>
<div class="flex flex-wrap items-center w-full gap-2"> <div class="flex flex-wrap items-center w-full gap-2">
{% for technology in job.technologies %} {{ chip::chip(text=technology) {% for technology in job.technologies %} {{ chip::chip(text=technology) }}
}} {% endfor %} {% endfor %}
</div> </div>
</div> </div>
{%endfor%} {%endfor%}

14
compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
website:
build: .
container_name: gabrielkaszewski-website
restart: unless-stopped
ports:
- "80:5150"
volumes:
- ./database:/app/db
- ./uploads:/app/uploads
environment:
- JWT_SECRET=your_super_secret_production_jwt_key_here
- HOST=https://your-domain.com
- BINDING=0.0.0.0

View File

@@ -7,7 +7,7 @@ logger:
# Enable pretty backtrace (sets RUST_BACKTRACE=1) # Enable pretty backtrace (sets RUST_BACKTRACE=1)
pretty_backtrace: true pretty_backtrace: true
# Log level, options: trace, debug, info, warn or error. # Log level, options: trace, debug, info, warn or error.
level: debug level: {{ get_env(name="LOGGER_LEVEL", default="debug") }}
# Define the logging format. options: compact, pretty or json # Define the logging format. options: compact, pretty or json
format: compact format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
@@ -19,6 +19,7 @@ server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT} # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 5150 port: 5150
# The UI hostname or IP address that mailers will point to. # The UI hostname or IP address that mailers will point to.
binding: {{ get_env(name="BINDING", default="localhost") }}
host: {{ get_env(name="HOST", default="http://localhost") }} host: {{ get_env(name="HOST", default="http://localhost") }}
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares: middlewares:
@@ -53,25 +54,6 @@ server:
path: "assets/static" path: "assets/static"
fallback: "assets/static/404.html" fallback: "assets/static/404.html"
#
# (2) Client side app static config
# =================================
#
# Note that you need to go in `frontend` and run your frontend build first,
# e.g.: $ npm install & npm build
#
# (client-block-start)
# static:
# enable: true
# must_exist: true
# precompressed: false
# folder:
# uri: "/"
# path: "frontend/dist"
# fallback: "frontend/dist/index.html"
# (client-block-end)
#
# Worker Configuration # Worker Configuration
workers: workers:
# specifies the worker mode. Options: # specifies the worker mode. Options:
@@ -106,7 +88,7 @@ mailer:
# Database Configuration # Database Configuration
database: database:
# Database connection URI # Database connection URI
uri: {{ get_env(name="DATABASE_URL", default="postgres://postgres:postgres@localhost:5432/gabrielkaszewski_rs") }} uri: {{ get_env(name="DATABASE_URL", default="sqlite://./database.db?mode=rwc") }}
# When enabled, the sql query will be logged. # When enabled, the sql query will be logged.
enable_logging: false enable_logging: false
# Set the timeout duration when acquiring a connection. # Set the timeout duration when acquiring a connection.

View File

@@ -1,18 +1,51 @@
FROM rust:1.74-slim as builder # =================================================================
# Stage 1: Build the Rust application
# =================================================================
FROM rust:1.89-slim-bookworm AS builder
WORKDIR /usr/src/ RUN apt-get update && apt-get install -y libsqlite3-dev pkg-config build-essential
COPY . . WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY .cargo ./.cargo/
COPY migration ./migration
RUN mkdir -p src/bin && \
echo "fn main() {}" > src/bin/main.rs && \
echo "fn main() {}" > src/bin/tool.rs
RUN cargo build --release RUN cargo build --release
FROM debian:bookworm-slim COPY src ./src
COPY assets ./assets
COPY config ./config
RUN cargo build --release
WORKDIR /usr/app # =================================================================
# Stage 2: Create the final, lightweight runtime image
# =================================================================
FROM debian:bookworm-slim AS runtime
COPY --from=builder /usr/src/assets/static /usr/app/assets/static RUN apt-get update && apt-get install -y libsqlite3-0 libssl3 gosu && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/assets/static/404.html /usr/app/assets/static/404.html
COPY --from=builder /usr/src/config /usr/app/config
COPY --from=builder /usr/src/target/release/gabrielkaszewski_rs-cli /usr/app/gabrielkaszewski_rs-cli
ENTRYPOINT ["/usr/app/gabrielkaszewski_rs-cli", "start"] RUN addgroup --system nonroot && adduser --system --ingroup nonroot nonroot
WORKDIR /app
COPY --from=builder /app/target/release/gabrielkaszewski_rs-cli ./server
COPY assets ./assets
COPY config ./config
RUN mkdir -p /app/db /app/uploads && chown -R nonroot:nonroot /app/db /app/uploads
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENV LOCO_ENV=production
ENV DATABASE_URL=sqlite:///app/db/production.db?mode=rwc
EXPOSE 5150
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["./server", "start"]

6
entrypoint.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
chown -R nonroot:nonroot /app/db /app/uploads
exec gosu nonroot "$@"

View File

@@ -13,47 +13,26 @@ enum Projects {
#[async_trait::async_trait] #[async_trait::async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
//
// add column
//
manager manager
.alter_table( .alter_table(
Table::alter() Table::alter()
.table(Projects::Table) .table(Projects::Table)
.add_column_if_not_exists(boolean(Projects::IsHighlighted).default(false)) .add_column_if_not_exists(boolean(Projects::IsHighlighted).default(false))
.add_column_if_not_exists(boolean(Projects::IsArchived).default(false))
.to_owned(), .to_owned(),
) )
.await .await?;
// // Add `is_archived` column
// delete column
//
/*
manager manager
.alter_table( .alter_table(
Table::alter() Table::alter()
.table(Movies::Table) .table(Projects::Table)
.drop_column(Movies::Rating) .add_column_if_not_exists(boolean(Projects::IsArchived).default(false))
.to_owned(), .to_owned(),
) )
.await .await?;
*/
// Ok(())
// create index
//
/*
manager
.create_index(
Index::create()
.name("idx-movies-rating")
.table(Movies::Table)
.col(Movies::Rating)
.to_owned(),
)
.await;
*/
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {

View File

@@ -48,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::job::routes())
.add_route(controllers::project::routes()) .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())
@@ -77,6 +78,7 @@ impl Hooks for App {
tasks.register(tasks::create_user::CreateUserData); tasks.register(tasks::create_user::CreateUserData);
tasks.register(tasks::create_job::CreateJobData); tasks.register(tasks::create_job::CreateJobData);
tasks.register(tasks::create_skill::CreateSkillData); tasks.register(tasks::create_skill::CreateSkillData);
tasks.register(tasks::import_skills::ImportSkills);
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);

View File

@@ -14,7 +14,7 @@ use axum_extra::TypedHeader;
use crate::models::users; use crate::models::users;
use crate::services; use crate::services;
pub async fn get_data( async fn get_data(
auth: Option<auth::JWT>, auth: Option<auth::JWT>,
range: Option<TypedHeader<Range>>, range: Option<TypedHeader<Range>>,
Path(file_name): Path<String>, Path(file_name): Path<String>,
@@ -23,7 +23,7 @@ pub async fn get_data(
services::data::serve_data_file(&auth, range, &file_name, &ctx).await services::data::serve_data_file(&auth, range, &file_name, &ctx).await
} }
pub async fn upload_data( async fn upload_data(
auth: auth::JWT, auth: auth::JWT,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
payload: Multipart, payload: Multipart,
@@ -37,9 +37,24 @@ pub async fn upload_data(
format::html("<h1>File uploaded successfully</h1>") format::html("<h1>File uploaded successfully</h1>")
} }
async fn delete_data(
auth: auth::JWT,
State(ctx): State<AppContext>,
Path(file_id): Path<i32>,
) -> Result<Response> {
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
Ok(_) => {}
Err(_) => return unauthorized("Unauthorized"),
}
services::data::delete_data_by_id(file_id, &ctx).await?;
format::html("<h1>File deleted successfully</h1>")
}
pub fn routes() -> Routes { pub fn routes() -> Routes {
Routes::new() Routes::new()
.prefix("api/data") .prefix("api/data")
.add("/upload", post(upload_data)) .add("/upload", post(upload_data))
.add("/:file_name", get(get_data)) .add("/:file_name", get(get_data))
.add("/id/:file_id", delete(delete_data))
} }

27
src/controllers/job.rs Normal file
View File

@@ -0,0 +1,27 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use crate::{
models::{jobs::CreateJobForm, users},
services,
};
async fn create_job(
auth: auth::JWT,
State(ctx): State<AppContext>,
Form(job_data): Form<CreateJobForm>,
) -> Result<Response> {
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
Ok(_) => {}
Err(_) => return unauthorized("Unauthorized"),
}
let job = services::jobs::create_job_from_form(&ctx, &job_data).await?;
format::json(&job)
}
pub fn routes() -> Routes {
Routes::new().prefix("api/jobs/").add("/", post(create_job))
}

View File

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

View File

@@ -80,6 +80,19 @@ pub async fn render_data(
views::data::list(v, &ctx).await views::data::list(v, &ctx).await
} }
async fn render_create_job(
auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
Ok(_) => {}
Err(_) => return unauthorized("Unauthorized"),
}
views::job::create_job(v).await
}
pub fn routes() -> Routes { pub fn routes() -> Routes {
Routes::new() Routes::new()
.add("/", get(render_index)) .add("/", get(render_index))
@@ -89,6 +102,7 @@ pub fn routes() -> Routes {
.add("/projects/create", get(render_create_project)) .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("/jobs/create", get(render_create_job))
.add("/data", get(render_data)) .add("/data", get(render_data))
.add("/about", get(render_about)) .add("/about", get(render_about))
} }

View File

@@ -1,6 +1,8 @@
use super::_entities::jobs::{ActiveModel, Entity}; use super::_entities::jobs::{ActiveModel, Entity};
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::NaiveDate;
pub type Jobs = Entity; pub type Jobs = Entity;
impl ActiveModelBehavior for ActiveModel { impl ActiveModelBehavior for ActiveModel {
@@ -17,3 +19,14 @@ pub struct JobWithTechnologies {
pub technologies: Vec<String>, pub technologies: Vec<String>,
pub still_working: bool, pub still_working: bool,
} }
#[derive(Serialize, Deserialize)]
pub struct CreateJobForm {
pub position: String,
pub company: String,
pub start_date: NaiveDate,
#[serde(default)]
pub end_date: Option<NaiveDate>,
pub technologies: String,
pub still_working: bool,
}

View File

@@ -109,7 +109,8 @@ pub async fn add(
let mut protected = None; let mut protected = None;
let mut file_name = None; let mut file_name = None;
let mut content = None; let mut content = None;
let mut file_path = None; let mut unique_name = None;
let mut ext = None;
while let Some(field) = payload while let Some(field) = payload
.next_field() .next_field()
@@ -128,25 +129,23 @@ pub async fn add(
.parse::<bool>() .parse::<bool>()
.map_err(|_| ModelError::Any("Failed to parse bool".into()))?; .map_err(|_| ModelError::Any("Failed to parse bool".into()))?;
protected = Some(value); protected = Some(value);
} },
"unique_name" => {
let value = field
.text()
.await
.map_err(|_| ModelError::Any("Failed to get text".into()))?
.parse::<bool>()
.map_err(|_| ModelError::Any("Failed to parse bool".into()))?;
unique_name = Some(value);
},
"file" => { "file" => {
let (og_file_name, ext) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?; let (og_file_name, extension) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?;
tracing::info!("File name: {:?}", og_file_name); let og_file_name = format!("{}.{}", og_file_name, extension);
ext = Some(extension.clone());
let temp_file_name = if uuid_name { file_name = Some(og_file_name.clone());
let temp_file_name = uuid::Uuid::new_v4().to_string();
format!("{}.{}", temp_file_name, ext)
} else {
og_file_name.to_string()
};
tracing::info!("Temp file name: {:?}", temp_file_name);
file_name = Some(temp_file_name.clone());
let path = PathBuf::from(temp_file_name);
file_path = Some(path.clone());
let data_content = field let data_content = field
.bytes() .bytes()
@@ -161,9 +160,20 @@ pub async fn add(
let protected = let protected =
protected.ok_or_else(|| ModelError::Any("Protected field is required".into()))?; protected.ok_or_else(|| ModelError::Any("Protected field is required".into()))?;
let unique_name = unique_name.unwrap_or(true);
let file_name = file_name.ok_or_else(|| ModelError::Any("File field is required".into()))?; let file_name = file_name.ok_or_else(|| ModelError::Any("File field is required".into()))?;
let file_name = match (uuid_name, unique_name) {
(true, true) => {
let temp_file_name = uuid::Uuid::new_v4().to_string();
format!("{}.{}", temp_file_name, ext.unwrap_or("txt".to_string()))
}
_ => file_name,
};
let path = PathBuf::from(file_name.clone());
let mut item = ActiveModel { let mut item = ActiveModel {
..Default::default() ..Default::default()
}; };
@@ -174,13 +184,12 @@ pub async fn add(
let item = item.insert(&ctx.db).await?; let item = item.insert(&ctx.db).await?;
let file_path = file_path.ok_or_else(|| ModelError::Any("File path is required".into()))?;
let content = content.ok_or_else(|| ModelError::Any("Content is required".into()))?; let content = content.ok_or_else(|| ModelError::Any("Content is required".into()))?;
match ctx match ctx
.storage .storage
.as_ref() .as_ref()
.upload(file_path.as_path(), &content) .upload(path.as_path(), &content)
.await .await
{ {
Ok(_) => {} Ok(_) => {}
@@ -232,7 +241,7 @@ pub async fn delete_data_by_id(id: i32, ctx: &AppContext) -> ModelResult<()> {
match data { match data {
Some(data) => { Some(data) => {
let path = PathBuf::from(&data.file_url); let path = PathBuf::from(&data.file_name);
match ctx.storage.as_ref().delete(&path).await { match ctx.storage.as_ref().delete(&path).await {
Ok(_) => {} Ok(_) => {}
Err(_) => return Err(ModelError::Any("Failed to delete file from storage".into())), Err(_) => return Err(ModelError::Any("Failed to delete file from storage".into())),

View File

@@ -2,8 +2,8 @@ use loco_rs::prelude::*;
use sea_orm::QueryOrder; use sea_orm::QueryOrder;
use crate::{models::{ use crate::{models::{
_entities::jobs::{Column, Entity, Model}, _entities::jobs::{ActiveModel, Column, Entity, Model},
jobs::JobWithTechnologies, jobs::{CreateJobForm, JobWithTechnologies},
}, shared::get_technologies_from_string::get_technologies_from_string}; }, shared::get_technologies_from_string::get_technologies_from_string};
pub async fn get_all_jobs(ctx: &AppContext) -> Result<Vec<Model>> { pub async fn get_all_jobs(ctx: &AppContext) -> Result<Vec<Model>> {
@@ -32,3 +32,19 @@ pub async fn get_all_jobs_with_technologies(ctx: &AppContext) -> Result<Vec<JobW
.collect(); .collect();
Ok(jobs_with_technologies) Ok(jobs_with_technologies)
} }
pub async fn create_job_from_form(ctx: &AppContext, job_data: &CreateJobForm) -> Result<Model> {
let new_job = ActiveModel {
company: Set(job_data.company.clone()),
position: Set(job_data.position.clone()),
start_date: Set(job_data.start_date.clone()),
end_date: Set(job_data.end_date.clone()),
technologies: Set(job_data.technologies.clone()),
still_working: Set(job_data.still_working),
..Default::default()
};
let job = new_job.insert(&ctx.db).await?;
Ok(job)
}

View File

@@ -1,7 +1,7 @@
use loco_rs::prelude::*; use loco_rs::prelude::*;
use sea_orm::QueryOrder; use sea_orm::QueryOrder;
use crate::models::_entities::skills::{Column, Entity, Model}; use crate::models::_entities::skills::{ActiveModel, Column, Entity, Model};
pub async fn get_all_skills(ctx: &AppContext) -> Result<Vec<Model>> { pub async fn get_all_skills(ctx: &AppContext) -> Result<Vec<Model>> {
let skills = Entity::find() let skills = Entity::find()
@@ -9,3 +9,25 @@ pub async fn get_all_skills(ctx: &AppContext) -> Result<Vec<Model>> {
.all(&ctx.db).await?; .all(&ctx.db).await?;
Ok(skills) Ok(skills)
} }
pub async fn add_skill(ctx: &AppContext, name: String) -> Result<Model> {
let new_skill = ActiveModel {
name: Set(name),
..Default::default()
};
let new_skill = new_skill.insert(&ctx.db).await?;
Ok(new_skill)
}
pub async fn add_skills(ctx: &AppContext, skills: Vec<String>) -> Result<Vec<Model>> {
let mut new_skills = vec![];
for skill in skills {
let new_skill = ActiveModel {
name: Set(skill),
..Default::default()
};
let new_skill = new_skill.insert(&ctx.db).await?;
new_skills.push(new_skill);
}
Ok(new_skills)
}

View File

@@ -1,6 +1,6 @@
use loco_rs::prelude::*; use loco_rs::prelude::*;
use crate::models::_entities::skills::ActiveModel; use crate::services::skills::add_skill;
pub struct CreateSkillData; pub struct CreateSkillData;
@@ -16,13 +16,7 @@ impl Task for CreateSkillData {
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
let name = vars.cli_arg("name")?; let name = vars.cli_arg("name")?;
let mut item = ActiveModel { let item = add_skill(app_context, name.to_string()).await?;
..Default::default()
};
item.name = Set(name.to_string());
let item = item.insert(&app_context.db).await?;
tracing::info!( tracing::info!(
skill_id = item.id, skill_id = item.id,

View File

@@ -0,0 +1,70 @@
use loco_rs::prelude::*;
use serde::Deserialize;
use crate::services::skills::add_skills;
pub struct ImportSkills;
#[derive(Deserialize)]
struct Skill {
name: String,
}
#[derive(Deserialize)]
struct Skills {
skills: Vec<Skill>,
}
#[async_trait]
impl Task for ImportSkills {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "import_skills".to_string(),
detail: "Task for importing skills from json file or stdin".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
let from_file = vars.cli_arg("from_file").ok().map(|v| v.parse::<bool>().unwrap_or(false)).unwrap_or(false);
match from_file {
true => {
let file_path = vars.cli_arg("file_path")?;
let data = std::fs::read_to_string(file_path)?;
let skills: Skills = serde_json::from_str(&data)?;
process_skills(app_context, skills).await?;
Ok(())
},
false => {
let raw_data = vars.cli_arg("raw_data")?;
let skills: Skills = get_skills_from_raw_data(&raw_data);
process_skills(app_context, skills).await?;
Ok(())
},
}
}
}
fn get_skills_from_raw_data(raw_data: &str) -> Skills {
let skills = raw_data.split(',').map(|s| Skill { name: s.to_string() }).collect();
Skills { skills }
}
async fn process_skills(app_context: &AppContext, skills: Skills) -> Result<()> {
let skills_names = skills.skills.iter().map(|s| s.name.clone()).collect();
let items = add_skills(app_context, skills_names).await?;
for item in items {
tracing::info!(
skill_id = item.id,
skill_name = &item.name,
"Skill created successfully",
);
}
Ok(())
}

View File

@@ -6,3 +6,4 @@ pub mod seed;
pub mod clear_data; pub mod clear_data;
pub mod delete_data; pub mod delete_data;
pub mod delete_project; pub mod delete_project;
pub mod import_skills;

5
src/views/job.rs Normal file
View File

@@ -0,0 +1,5 @@
use loco_rs::prelude::*;
pub async fn create_job(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::render().view(&v, "website/create-job.html", data!({}))
}

View File

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