better projects upload and mobile design
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,9 +1,14 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const carousels = document.querySelectorAll('[id^="carousel-"]');
|
const carousels = document.querySelectorAll('[id^="carousel-"]');
|
||||||
|
|
||||||
carousels.forEach((carousel) => {
|
carousels.forEach((carousel) => {
|
||||||
let buttons = carousel.querySelectorAll('.carousel-button');
|
let buttons = carousel.querySelectorAll('.carousel-button');
|
||||||
let activeSlide = 0;
|
let activeSlide = 0;
|
||||||
|
|
||||||
|
if (buttons.length === 1) {
|
||||||
|
buttons[0].classList.add('opacity-0');
|
||||||
|
}
|
||||||
|
|
||||||
buttons.forEach((button, index) => {
|
buttons.forEach((button, index) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
let currentSlide = carousel.querySelector(
|
let currentSlide = carousel.querySelector(
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{% extends "website/base.html" %} {% block content %}
|
{% extends "website/base.html" %} {% block content %}
|
||||||
<div class="w-full mt-16"></div>
|
<div class="w-full mt-16"></div>
|
||||||
<form method="post" id="project-upload" class="flex flex-col gap-2 text-black" action="/api/projects">
|
<form method="post" id="project-upload" class="flex flex-col gap-2 text-black" action="/api/projects/upload"
|
||||||
|
enctype="multipart/form-data">
|
||||||
<label class="text-white" for="name">Project Name:</label>
|
<label class="text-white" for="name">Project Name:</label>
|
||||||
<input type="text" id="name" name="name" required />
|
<input type="text" id="name" name="name" required />
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@
|
|||||||
<label class="text-white" for="technology">Technologies:</label>
|
<label class="text-white" for="technology">Technologies:</label>
|
||||||
<input type="text" id="technology" name="technologies" required />
|
<input type="text" id="technology" name="technologies" required />
|
||||||
|
|
||||||
|
<label for="files" class="text-white">Choose thumbnails files:</label>
|
||||||
|
<input type="file" id="files" name="thumbnail" multiple accept="image/*">
|
||||||
|
|
||||||
<button type="submit" class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600">Submit</button>
|
<button type="submit" class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content%}
|
{% endblock content%}
|
@@ -1,12 +1,13 @@
|
|||||||
{% import "website/macros/chip.html" as chip %} {% extends "website/base.html"
|
{% import "website/macros/chip.html" as chip %} {% extends "website/base.html"
|
||||||
%} {% block content %}
|
%} {% block content %}
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
|
<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 src="/static/images/optimized-75.webp" alt="Background"
|
||||||
class="object-cover w-full max-h-full pointer-events-none" />
|
class="hidden object-cover w-full max-h-full pointer-events-none md:block" />
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 flex flex-col items-center justify-center w-full gap-4 md:items-start md:justify-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>
|
<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
|
||||||
@@ -16,7 +17,15 @@
|
|||||||
Full-Stack Developer
|
Full-Stack Developer
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="items-center hidden gap-2 md:flex motion-preset-slide-right motion-duration-2000">
|
<div class="md:hidden">
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-white motion-preset-slide-right motion-duration-1000">
|
||||||
|
Gabriel Kaszewski
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-lg font-light tracking-tight text-white motion-preset-slide-right motion-duration-1000">
|
||||||
|
Full-Stack Developer
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<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 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">
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -51,7 +60,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 p-2 text-sm">
|
<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 class="underline"
|
||||||
@@ -67,7 +76,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full mt-8"></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">
|
||||||
|
@@ -2,14 +2,14 @@
|
|||||||
<div id="{{ id }}" class="carousel relative shadow-lg w-full max-w-full md:max-w-[50hw] h-[40rem]">
|
<div id="{{ id }}" class="carousel relative shadow-lg w-full max-w-full md:max-w-[50hw] h-[40rem]">
|
||||||
<div class="relative w-full h-full overflow-hidden carousel-inner">
|
<div class="relative w-full h-full overflow-hidden carousel-inner">
|
||||||
{% for thumbnail in thumbnails %}
|
{% for thumbnail in thumbnails %}
|
||||||
<div class="carousel-item absolute inset-0 w-full h-full {% if forloop.first %}{% else %} opacity-0 {% endif %} transition-opacity ease-in-out delay-250 duration-500">
|
<div class="absolute inset-0 w-full h-full transition-opacity duration-500 ease-in-out carousel-item delay-250">
|
||||||
<img alt="slide" src="{{ thumbnail }}" class="object-cover w-full h-full">
|
<img alt="slide" src="{{ thumbnail }}" class="object-cover w-full h-full">
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 z-50 flex justify-center w-full gap-2 py-2">
|
<div class="absolute bottom-0 z-50 flex justify-center w-full gap-2 py-2">
|
||||||
{% for thumbnail in thumbnails %}
|
{% for thumbnail in thumbnails %}
|
||||||
<button class="carousel-button" data-target="{{ forloop.counter0 }}">•</button>
|
<button class="carousel-button" data-target="{{ loop.index0 }}">•</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -109,7 +109,7 @@ async fn login(
|
|||||||
|
|
||||||
let token = user
|
let token = user
|
||||||
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
||||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
.or_else(|_| unauthorized("jwt error"))?;
|
||||||
|
|
||||||
let cookie = Cookie::build(("token", token.clone()))
|
let cookie = Cookie::build(("token", token.clone()))
|
||||||
.path("/")
|
.path("/")
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
#![allow(clippy::missing_errors_doc)]
|
#![allow(clippy::missing_errors_doc)]
|
||||||
#![allow(clippy::unnecessary_struct_initialization)]
|
#![allow(clippy::unnecessary_struct_initialization)]
|
||||||
#![allow(clippy::unused_async)]
|
#![allow(clippy::unused_async)]
|
||||||
|
use axum::extract::Multipart;
|
||||||
|
use format::redirect;
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -39,8 +41,24 @@ async fn create_project(
|
|||||||
format::json(&project)
|
format::json(&project)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_project_with_thumbnails(
|
||||||
|
auth: auth::JWT,
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
payload: Multipart,
|
||||||
|
) -> Result<Response> {
|
||||||
|
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => return unauthorized("Unauthorized"),
|
||||||
|
}
|
||||||
|
|
||||||
|
services::projects::add_project_with_thumbnails_multipart(&auth, &ctx, payload).await?;
|
||||||
|
|
||||||
|
redirect("/projects")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
Routes::new()
|
Routes::new()
|
||||||
.prefix("api/projects/")
|
.prefix("api/projects/")
|
||||||
.add("/", post(create_project))
|
.add("/", post(create_project))
|
||||||
|
.add("/upload", post(create_project_with_thumbnails))
|
||||||
}
|
}
|
||||||
|
13
src/services/auth.rs
Normal file
13
src/services/auth.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
use crate::models::users;
|
||||||
|
|
||||||
|
pub async fn is_logged_in(
|
||||||
|
auth: &auth::JWT,
|
||||||
|
ctx: &AppContext,
|
||||||
|
) -> Result<bool> {
|
||||||
|
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use crate::models::_entities::data::{self, ActiveModel, Entity, Model};
|
use crate::models::_entities::data::{self, ActiveModel, Entity, Model};
|
||||||
use crate::models::users::users;
|
use crate::models::users::users;
|
||||||
|
use crate::shared::get_file_name_with_extension::get_file_name_with_extension_from_field;
|
||||||
use axum::extract::Multipart;
|
use axum::extract::Multipart;
|
||||||
use axum_extra::headers::Range;
|
use axum_extra::headers::Range;
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
@@ -129,10 +130,7 @@ pub async fn add(
|
|||||||
protected = Some(value);
|
protected = Some(value);
|
||||||
}
|
}
|
||||||
"file" => {
|
"file" => {
|
||||||
let og_file_name = field
|
let (og_file_name, ext) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?;
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| ModelError::Any("Failed to get file name".into()))?;
|
|
||||||
let ext = String::from(og_file_name.split('.').last().unwrap_or("txt"));
|
|
||||||
|
|
||||||
let temp_file_name = if uuid_name {
|
let temp_file_name = if uuid_name {
|
||||||
let temp_file_name = uuid::Uuid::new_v4().to_string();
|
let temp_file_name = uuid::Uuid::new_v4().to_string();
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
|
use sea_orm::QueryOrder;
|
||||||
|
|
||||||
use crate::{models::{
|
use crate::{models::{
|
||||||
_entities::jobs::{Entity, Model},
|
_entities::jobs::{Column, Entity, Model},
|
||||||
jobs::JobWithTechnologies,
|
jobs::JobWithTechnologies,
|
||||||
}, shared::get_technologies_from_string::get_technologies_from_string};
|
}, shared::get_technologies_from_string::get_technologies_from_string};
|
||||||
|
|
||||||
@@ -11,7 +12,9 @@ pub async fn get_all_jobs(ctx: &AppContext) -> Result<Vec<Model>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_jobs_with_technologies(ctx: &AppContext) -> Result<Vec<JobWithTechnologies>> {
|
pub async fn get_all_jobs_with_technologies(ctx: &AppContext) -> Result<Vec<JobWithTechnologies>> {
|
||||||
let jobs = Entity::find().all(&ctx.db).await?;
|
let jobs = Entity::find()
|
||||||
|
.order_by_asc(Column::StartDate)
|
||||||
|
.all(&ctx.db).await?;
|
||||||
let jobs_with_technologies = jobs
|
let jobs_with_technologies = jobs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|job| {
|
.map(|job| {
|
||||||
|
@@ -2,4 +2,5 @@ pub mod data;
|
|||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
pub mod skills;
|
pub mod skills;
|
||||||
pub mod website;
|
pub mod website;
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
|
pub mod auth;
|
@@ -1,13 +1,15 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use axum::extract::Multipart;
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
_entities::{
|
_entities::{
|
||||||
project_thumbnails,
|
data, project_thumbnails, projects::{self, ActiveModel, Entity, Model}
|
||||||
projects::{self, ActiveModel, Entity, Model},
|
|
||||||
},
|
},
|
||||||
projects::{get_category_from_string, get_string_from_category, CreateProject, ProjectDto, UpdateProject},
|
projects::{get_category_from_string, get_string_from_category, CreateProject, ProjectDto, UpdateProject}, users,
|
||||||
}, services::data::add_data_file_from_path, shared::get_technologies_from_string::get_technologies_from_string
|
}, services::data::add_data_file_from_path, shared::{get_file_name_with_extension::get_file_name_with_extension_from_field, get_technologies_from_string::get_technologies_from_string}
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::data::delete_data_by_id;
|
use super::data::delete_data_by_id;
|
||||||
@@ -62,13 +64,39 @@ pub async fn get_all_projects_dto(ctx: &AppContext) -> Result<Vec<ProjectDto>> {
|
|||||||
.all(&ctx.db)
|
.all(&ctx.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let thumbnails_ids = projects_with_thumbnails
|
||||||
|
.iter()
|
||||||
|
.map(|(_, thumbnails)| {
|
||||||
|
thumbnails
|
||||||
|
.iter()
|
||||||
|
.map(|thumbnail| thumbnail.data_id)
|
||||||
|
.collect::<Vec<i32>>()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<i32>>();
|
||||||
|
|
||||||
|
let thumbnails_data = data::Entity::find()
|
||||||
|
.filter(model::query::condition().is_in(data::Column::Id, thumbnails_ids).build())
|
||||||
|
.all(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let thumbnails_map = thumbnails_data
|
||||||
|
.into_iter()
|
||||||
|
.map(|thumbnail| (thumbnail.id, thumbnail))
|
||||||
|
.collect::<std::collections::HashMap<i32, data::Model>>();
|
||||||
|
|
||||||
let projects_dto = projects_with_thumbnails
|
let projects_dto = projects_with_thumbnails
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(project, thumbnails)| {
|
.map(|(project, thumbnails)| {
|
||||||
let thumbnails = thumbnails
|
let thumbnails = thumbnails
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|thumbnail| thumbnail.data_id.to_string())
|
.map(|thumbnail| {
|
||||||
|
let thumbnail_data = thumbnails_map.get(&thumbnail.data_id).unwrap();
|
||||||
|
let url = format!("/api/data/{}", thumbnail_data.file_name);
|
||||||
|
url
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
ProjectDto {
|
ProjectDto {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
@@ -189,6 +217,176 @@ pub async fn add_project_with_thumbnails(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_project_with_thumbnails_multipart(
|
||||||
|
auth: &auth::JWT,
|
||||||
|
ctx: &AppContext,
|
||||||
|
mut payload: Multipart,
|
||||||
|
) -> Result<()> {
|
||||||
|
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||||
|
|
||||||
|
let mut project_name = None;
|
||||||
|
let mut short_description = None;
|
||||||
|
let mut description = None;
|
||||||
|
let mut category = None;
|
||||||
|
let mut github_url = None;
|
||||||
|
let mut download_url = None;
|
||||||
|
let mut visit_url = None;
|
||||||
|
let mut technologies = None;
|
||||||
|
|
||||||
|
let mut thumbnails_file_names = Vec::new();
|
||||||
|
let mut thumbnails = Vec::new();
|
||||||
|
|
||||||
|
while let Some(field) = payload
|
||||||
|
.next_field()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get next field".into()))?
|
||||||
|
{
|
||||||
|
let name = field
|
||||||
|
.name()
|
||||||
|
.ok_or_else(|| ModelError::Any("Failed to get field name".into()))?;
|
||||||
|
|
||||||
|
match name {
|
||||||
|
"name" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
project_name = Some(value);
|
||||||
|
}
|
||||||
|
"short_description" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
short_description = Some(value);
|
||||||
|
}
|
||||||
|
"description" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
description = Some(value);
|
||||||
|
}
|
||||||
|
"category" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
category = Some(value);
|
||||||
|
}
|
||||||
|
"github_url" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
github_url = Some(value);
|
||||||
|
}
|
||||||
|
"download_url" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
download_url = Some(value);
|
||||||
|
}
|
||||||
|
"visit_url" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
visit_url = Some(value);
|
||||||
|
}
|
||||||
|
"technologies" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||||
|
technologies = Some(value);
|
||||||
|
}
|
||||||
|
"thumbnail" => {
|
||||||
|
let (_, ext) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?;
|
||||||
|
|
||||||
|
let file_name = uuid::Uuid::new_v4().to_string();
|
||||||
|
let file_name = format!("{}.{}", file_name, ext);
|
||||||
|
|
||||||
|
let data_content = field
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get bytes".into()))?;
|
||||||
|
|
||||||
|
thumbnails_file_names.push(file_name);
|
||||||
|
thumbnails.push(data_content);
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let category = category.map(|s| get_category_from_string(&s));
|
||||||
|
|
||||||
|
let project = CreateProject {
|
||||||
|
name: project_name.ok_or_else(|| ModelError::Any("Name field is required".into()))?,
|
||||||
|
short_description: short_description.ok_or_else(|| ModelError::Any("Short description field is required".into()))?,
|
||||||
|
description: description,
|
||||||
|
category: category.ok_or_else(|| ModelError::Any("Category field is required".into()))?,
|
||||||
|
github_url: github_url,
|
||||||
|
download_url: download_url,
|
||||||
|
visit_url: visit_url,
|
||||||
|
technologies: technologies.ok_or_else(|| ModelError::Any("Technologies field is required".into()))?.split(",").map(|s| s.to_string()).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let txn = ctx.db.begin().await?;
|
||||||
|
|
||||||
|
let item = ActiveModel {
|
||||||
|
name: Set(project.name),
|
||||||
|
short_description: Set(project.short_description),
|
||||||
|
description: Set(project.description),
|
||||||
|
category: Set(get_string_from_category(&project.category)),
|
||||||
|
github_url: Set(project.github_url),
|
||||||
|
download_url: Set(project.download_url),
|
||||||
|
visit_url: Set(project.visit_url),
|
||||||
|
technology: Set(project.technologies.join(",")),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = item.insert(&txn).await?;
|
||||||
|
|
||||||
|
let project_id = item.id;
|
||||||
|
|
||||||
|
for (thumbnail_file_name, thumbnail) in thumbnails_file_names.iter().zip(thumbnails.iter()) {
|
||||||
|
let thumbnail_data = data::ActiveModel {
|
||||||
|
file_name: Set(thumbnail_file_name.clone()),
|
||||||
|
file_url: Set(format!("uploads/{}", thumbnail_file_name)),
|
||||||
|
protected: Set(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let thumbnail_data = thumbnail_data.insert(&txn).await?;
|
||||||
|
let path = PathBuf::from(thumbnail_file_name);
|
||||||
|
match ctx
|
||||||
|
.storage
|
||||||
|
.as_ref()
|
||||||
|
.upload(
|
||||||
|
path.as_path(),
|
||||||
|
thumbnail,
|
||||||
|
)
|
||||||
|
.await {
|
||||||
|
Ok(_) => {
|
||||||
|
let thumbnail = project_thumbnails::ActiveModel {
|
||||||
|
project_id: Set(project_id),
|
||||||
|
data_id: Set(thumbnail_data.id),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
thumbnail.insert(&txn).await?;
|
||||||
|
},
|
||||||
|
Err(_) => return Err(Error::Any("Failed to save file to storage".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_project(
|
pub async fn update_project(
|
||||||
ctx: &AppContext,
|
ctx: &AppContext,
|
||||||
id: i32,
|
id: i32,
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
|
use sea_orm::QueryOrder;
|
||||||
|
|
||||||
use crate::models::_entities::skills::{Entity, Model};
|
use crate::models::_entities::skills::{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().all(&ctx.db).await?;
|
let skills = Entity::find()
|
||||||
|
.order_by_asc(Column::Name)
|
||||||
|
.all(&ctx.db).await?;
|
||||||
Ok(skills)
|
Ok(skills)
|
||||||
}
|
}
|
||||||
|
13
src/shared/get_file_name_with_extension.rs
Normal file
13
src/shared/get_file_name_with_extension.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use axum::extract::multipart::Field;
|
||||||
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
pub fn get_file_name_with_extension_from_field(
|
||||||
|
field: &Field<'_>,
|
||||||
|
default_extension: &str,
|
||||||
|
) -> Result<(String, String)> {
|
||||||
|
let file_name = field.file_name().ok_or_else(|| ModelError::Any("Failed to get file name".into()))?;
|
||||||
|
let mut parts = file_name.split('.').collect::<Vec<&str>>();
|
||||||
|
let extension = parts.pop().unwrap_or(default_extension);
|
||||||
|
let file_name = parts.join(".");
|
||||||
|
Ok((file_name, extension.to_string()))
|
||||||
|
}
|
@@ -1 +1,2 @@
|
|||||||
pub mod get_technologies_from_string;
|
pub mod get_technologies_from_string;
|
||||||
|
pub mod get_file_name_with_extension;
|
@@ -41,6 +41,6 @@ impl CurrentResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||||
format::render().view(&v, "website/login.html", data!({}))
|
format::render().view(&v, "website/login.html", data!({}))
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user