diff --git a/assets/tailwind.css b/assets/tailwind.css index 5495a1a..2d3fbc4 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -903,22 +903,6 @@ html { color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); } -.checkbox { - flex-shrink: 0; - --chkbg: var(--fallback-bc,oklch(var(--bc)/1)); - --chkfg: var(--fallback-b1,oklch(var(--b1)/1)); - height: 1.5rem; - width: 1.5rem; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-border-opacity: 0.2; -} - @media (hover: hover) { .btm-nav > *.disabled:hover, .btm-nav > *[disabled]:hover { @@ -1410,59 +1394,20 @@ input.tab:checked + .tab-content, line-height: 1.25rem; } +.card-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.25rem; + line-height: 1.75rem; + font-weight: 600; +} + .card.image-full :where(figure) { overflow: hidden; border-radius: inherit; } -.checkbox:focus { - box-shadow: none; -} - -.checkbox:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); -} - -.checkbox:disabled { - border-width: 0px; - cursor: not-allowed; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - opacity: 0.2; -} - -.checkbox:checked, - .checkbox[aria-checked="true"] { - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-out; - background-color: var(--chkbg); - background-image: linear-gradient(-45deg, transparent 65%, var(--chkbg) 65.99%), - linear-gradient(45deg, transparent 75%, var(--chkbg) 75.99%), - linear-gradient(-45deg, var(--chkbg) 40%, transparent 40.99%), - linear-gradient( - 45deg, - var(--chkbg) 30%, - var(--chkfg) 30.99%, - var(--chkfg) 40%, - transparent 40.99% - ), - linear-gradient(-45deg, var(--chkfg) 50%, var(--chkbg) 50.99%); -} - -.checkbox:indeterminate { - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-out; - background-image: linear-gradient(90deg, transparent 80%, var(--chkbg) 80%), - linear-gradient(-90deg, transparent 80%, var(--chkbg) 80%), - linear-gradient(0deg, var(--chkbg) 43%, var(--chkfg) 43%, var(--chkfg) 57%, var(--chkbg) 57%); -} - @keyframes checkmark { 0% { background-position-y: 5px; @@ -1895,32 +1840,6 @@ input.tab:checked + .tab-content, } } -.btn-sm { - height: 2rem; - min-height: 2rem; - padding-left: 0.75rem; - padding-right: 0.75rem; - font-size: 0.875rem; -} - -.btn-square:where(.btn-sm) { - height: 2rem; - width: 2rem; - padding: 0px; -} - -.btn-circle:where(.btn-sm) { - height: 2rem; - width: 2rem; - border-radius: 9999px; - padding: 0px; -} - -[type="checkbox"].checkbox-sm { - height: 1.25rem; - width: 1.25rem; -} - .tabs-md :where(.tab) { height: 2rem; font-size: 0.875rem; @@ -1957,12 +1876,20 @@ input.tab:checked + .tab-content, line-height: 1.25rem; } +.card-compact .card-title { + margin-bottom: 0.25rem; +} + .card-normal .card-body { padding: var(--padding-card, 2rem); font-size: 1rem; line-height: 1.5rem; } +.card-normal .card-title { + margin-bottom: 0.75rem; +} + .static { position: static; } @@ -1972,6 +1899,10 @@ input.tab:checked + .tab-content, margin-right: auto; } +.mt-10 { + margin-top: 2.5rem; +} + .mt-20 { margin-top: 5rem; } @@ -1980,22 +1911,6 @@ input.tab:checked + .tab-content, margin-top: 1.5rem; } -.mt-10 { - margin-top: 2.5rem; -} - -.mt-5 { - margin-top: 1.25rem; -} - -.mt-16 { - margin-top: 4rem; -} - -.mt-8 { - margin-top: 2rem; -} - .flex { display: flex; } @@ -2008,24 +1923,40 @@ input.tab:checked + .tab-content, width: 100%; } -.max-w-2xl { - max-width: 42rem; +.w-96 { + width: 24rem; } -.max-w-lg { - max-width: 32rem; +.w-\[25rem\] { + width: 25rem; +} + +.max-w-2xl { + max-width: 42rem; } .max-w-4xl { max-width: 56rem; } +.max-w-lg { + max-width: 32rem; +} + .max-w-md { max-width: 28rem; } -.max-w-xl { - max-width: 36rem; +.max-w-5xl { + max-width: 64rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-7xl { + max-width: 80rem; } .flex-1 { @@ -2036,20 +1967,20 @@ input.tab:checked + .tab-content, flex: none; } -.justify-start { - justify-content: flex-start; +.flex-wrap { + flex-wrap: wrap; } .gap-5 { gap: 1.25rem; } -.gap-2 { - gap: 0.5rem; +.gap-10 { + gap: 2.5rem; } -.gap-1 { - gap: 0.25rem; +.gap-8 { + gap: 2rem; } .bg-base-100 { @@ -2067,6 +1998,11 @@ input.tab:checked + .tab-content, padding-right: 0.25rem; } +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; @@ -2077,11 +2013,6 @@ input.tab:checked + .tab-content, padding-bottom: 1.5rem; } -.px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; -} - .pt-40 { padding-top: 10rem; } @@ -2090,6 +2021,11 @@ input.tab:checked + .tab-content, text-align: center; } +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + .text-5xl { font-size: 3rem; line-height: 1; @@ -2100,11 +2036,6 @@ input.tab:checked + .tab-content, line-height: 1.75rem; } -.text-2xl { - font-size: 1.5rem; - line-height: 2rem; -} - .font-bold { font-weight: 700; } @@ -2117,4 +2048,46 @@ input.tab:checked + .tab-content, .filter { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.hover\:cursor-pointer:hover { + cursor: pointer; +} + +.hover\:bg-base-200:hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); +} + +.hover\:bg-base-200\/80:hover { + background-color: var(--fallback-b2,oklch(var(--b2)/0.8)); +} + +.hover\:bg-base-200\/60:hover { + background-color: var(--fallback-b2,oklch(var(--b2)/0.6)); +} + +.hover\:bg-base-200\/50:hover { + background-color: var(--fallback-b2,oklch(var(--b2)/0.5)); +} + +.hover\:bg-base-300\/100:hover { + background-color: var(--fallback-b3,oklch(var(--b3)/1)); +} + +.hover\:bg-base-300:hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); +} + +.hover\:shadow-xl:hover { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } \ No newline at end of file diff --git a/src/components/modpacks.rs b/src/components/modpacks.rs index cf8f83a..922645a 100644 --- a/src/components/modpacks.rs +++ b/src/components/modpacks.rs @@ -1,11 +1,59 @@ +use crate::server::modpack::Modpack; use dioxus::prelude::*; #[component] pub fn Modpacks() -> Element { + let modpacks = use_resource(|| async move { get_modpacks().await }); + rsx! { div { - class: "max-w-4xl px-2 w-full mx-auto", + class: "max-w-7xl px-2 w-full mx-auto", h2 { class: "text-2xl font-bold mt-10", "Modpacks" } + div { + class: "flex flex-wrap gap-8 mt-10", + match &*modpacks.read_unchecked() { + Some(Ok(response)) => rsx! { + for modpack in response { + ModpackCard { modpack: modpack.to_owned() } + } + }, + Some(Err(_)) => rsx! { "Loading modpacks failed" }, + None => rsx! { "Loading modpacks... " }, + } + } } } } + +#[derive(PartialEq, Props, Clone)] +pub struct ModpackCardProps { + modpack: Modpack, +} + +#[component] +pub fn ModpackCard(props: ModpackCardProps) -> Element { + rsx! { + div { + class: "card w-[25rem] bg-base-200 hover:bg-base-300 hover:cursor-pointer shadow-xl transition-all", + div { + class: "card-body", + h2 { class: "card-title", "{props.modpack.name}" }, + div { + "For Minecraft ", + b { "{props.modpack.game_version}" }, + " with ", + b { "{props.modpack.modloader}" }, + } + } + } + } +} + +#[server] +async fn get_modpacks() -> Result, ServerFnError> { + use crate::server::auth::User; + + let user = User::from_cookie().await?; + + Modpack::get_all(user.id).await +} diff --git a/src/components/new.rs b/src/components/new.rs index bcb0460..90d4f00 100644 --- a/src/components/new.rs +++ b/src/components/new.rs @@ -1,9 +1,12 @@ use dioxus::prelude::*; use crate::modrinth::tags::game_versions::{get_game_versions, GameVersion}; +use crate::Route; #[component] pub fn New() -> Element { + let nav = navigator(); + let game_versions = use_resource(|| async move { let response = get_game_versions().await; @@ -16,17 +19,31 @@ pub fn New() -> Element { } }); + let submit = move |event: FormEvent| async move { + if let Ok(_modpack_id) = create_modpack( + event.values()["name"].as_value(), + event.values()["game_version"].as_value(), + event.values()["modloader"].as_value(), + ) + .await + { + nav.push(Route::Modpacks {}); + } + }; + rsx! { div { class: "max-w-md w-full mx-auto", h2 { class: "text-2xl font-bold mt-20", "Create a new modpack" } form { class: "mt-10", - input { r#type: "text", placeholder: "Name", class: "input input-bordered w-full" } + onsubmit: submit, + input { r#type: "text", name: "name", placeholder: "Name", class: "input input-bordered w-full" } div { class: "flex gap-5 mt-6", select { class: "select select-bordered w-full", + name: "game_version", option { disabled: true, selected: true, "Minecraft Version" }, match &*game_versions.read_unchecked() { Some(Ok(response)) => rsx! { @@ -39,6 +56,7 @@ pub fn New() -> Element { } select { class: "select select-bordered", + name: "modloader", option { disabled: true, selected: true, "Modloader" }, option { "Fabric" }, option { "Quilt" }, @@ -51,3 +69,19 @@ pub fn New() -> Element { } } } + +#[server] +async fn create_modpack( + name: String, + game_version: String, + modloader: String, +) -> Result { + use crate::server::auth::User; + use crate::server::modpack::Modpack; + + let user = User::from_cookie().await?; + + let modpack = Modpack::new(name, game_version, modloader, user.id).await?; + + Ok(modpack.id) +} diff --git a/src/server.rs b/src/server.rs index 905d811..207486e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod modpack; #[cfg(feature = "server")] pub mod surrealdb; diff --git a/src/server/auth.rs b/src/server/auth.rs index 352269f..a1faae5 100644 --- a/src/server/auth.rs +++ b/src/server/auth.rs @@ -28,7 +28,7 @@ struct Session { impl User { #[cfg(feature = "server")] - async fn from_cookie() -> Result { + pub async fn from_cookie() -> Result { use axum_extra::extract::CookieJar; let jar: CookieJar = extract().await?; @@ -40,7 +40,7 @@ impl User { let session_token = session_token.value(); let mut res = DB - .query("SELECT type::string(user.id) as id, user.email as email FROM session WHERE token = $session_token") + .query("SELECT type::string(user.id) as id, user.email as email FROM session WHERE token = $session_token") .bind(("session_token", session_token)) .await?; diff --git a/src/server/modpack.rs b/src/server/modpack.rs new file mode 100644 index 0000000..d4a0d2d --- /dev/null +++ b/src/server/modpack.rs @@ -0,0 +1,63 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "server")] +use super::surrealdb::DB; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Modpack { + pub id: String, + pub owner_id: String, + pub name: String, + pub game_version: String, + pub modloader: String, + pub projects: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Project { + pub project_id: String, + pub version_id: String, +} + +impl Modpack { + #[cfg(feature = "server")] + pub async fn new( + name: String, + game_version: String, + modloader: String, + user_id: String, + ) -> Result { + let mut res = DB + .query("CREATE modpack SET name = $name, owner = type::thing($user_id), game_version = $game_version, modloader = $modloader, projects = [] RETURN type::string(id) as id, type::string(owner) as owner_id, game_version, modloader, name, projects") + .bind(("name", name)) + .bind(("user_id", user_id)) + .bind(("game_version", game_version)) + .bind(("modloader", modloader)) + .await?; + + let modpack: Option = res.take(0)?; + + match modpack { + Some(m) => { + tracing::info!("Created new modpack: {}", m.id); + Ok(m) + } + None => Err(ServerFnError::ServerError( + "Could not create modpack".to_string(), + )), + } + } + + #[cfg(feature = "server")] + pub async fn get_all(user_id: String) -> Result, ServerFnError> { + let mut res = DB + .query("SELECT type::string(id) as id, type::string(owner) as owner_id, name, modloader, game_version, projects FROM modpack WHERE type::string(owner) = $owner_id;") + .bind(("owner_id", user_id)) + .await?; + + let modpacks: Vec = res.take(0)?; + + Ok(modpacks) + } +}