Compare commits

...

32 Commits

Author SHA1 Message Date
d200ece6cc CSS Fixes - Scrollbars, Album page, and making constants 2024-11-23 01:20:08 +00:00
9b48fc0204 Merge pull request '95-fix-home-screen-account-button-ui' (#124) from 95-fix-home-screen-account-button-ui into main
Reviewed-on: LibreTunes/LibreTunes#124
2024-11-22 22:32:53 +00:00
dfbb217988 Merge pull request '114-create-album-page' (#149) from 114-create-album-page into main
Reviewed-on: LibreTunes/LibreTunes#149
2024-11-22 22:31:27 +00:00
954cc0edce Artist links and styling 2024-11-22 22:13:43 +00:00
e5ce0eab76 Fix Build Warnings 2024-11-22 21:55:59 +00:00
45eb7191f0 Bugfix and small changes to styling 2024-11-22 21:47:24 +00:00
21a17a8eb5 Album Page Styling 2024-11-22 21:34:21 +00:00
fcd987d433 change unwrap or default to unwrap or false for readability 2024-11-22 16:31:55 -05:00
dd14aa0b4d AlbumData Query, API Endpoint, and Integration into AlbumPage 2024-11-20 04:43:53 +00:00
3b6035dd71 Album pages for users not signed in 2024-11-20 02:12:09 +00:00
22cee4a265 Merge remote-tracking branch 'origin/main' into 114-create-album-page 2024-11-19 22:21:46 +00:00
b25cb4549c Revert get_user to not be server function 2024-11-19 16:21:35 -05:00
73b4b7674e Use global state user instead of local resource in personal page 2024-11-17 18:42:57 -05:00
3d1dc94b06 Merge remote-tracking branch 'origin/main' into 95-fix-home-screen-account-button-ui 2024-11-17 17:38:29 -05:00
d8fab0e068 Reduce calls to get_user by not having logged_in resource 2024-11-12 17:11:06 -05:00
568f6ada0e Working Albums Page (BETA) 2024-11-12 21:44:30 +00:00
7a1ffaad47 Finished Query for Song Data from Album 2024-11-08 22:16:46 +00:00
f1affc66bc Remove email display on personal and rescale icon/pfp to be bigger & consistent 2024-11-01 17:17:59 -04:00
e60243e50c Redirect to login when logging out 2024-11-01 17:17:06 -04:00
ff24f68eed A very big SQL Query 2024-10-26 03:53:03 +00:00
6aa933be09 Refactor profile picture display to account for no profile picture 2024-10-25 22:59:06 -04:00
fe1b76e6e4 API Endpoints for Album Queries 2024-10-25 21:23:21 +00:00
acf15961cd Fix formatting and styling of profile picture display, as well as display email when logged in as well 2024-10-25 17:01:50 -04:00
df01bafbd1 Album Page Component 2024-10-22 03:32:55 +00:00
2d7b91413b Display username in profile container when logged in 2024-10-18 19:31:20 -04:00
c2ebd8307f Display user profile picture when logged in instead of generic profile icon 2024-10-18 19:30:45 -04:00
2be665c549 Track logged in status and user as local resource instead of signal 2024-10-18 19:26:10 -04:00
f104a14f98 Remove derive Default for User and make user signal Option type instead 2024-10-15 17:26:30 -04:00
f78066d7a8 Display username in profile when logged in 2024-10-15 17:18:41 -04:00
071dcad0cc Convert borders and margins to rem from px in profile 2024-10-15 17:17:44 -04:00
3149f65a97 Detect user logged in and display "logged in" for profile 2024-10-11 17:21:11 -04:00
ab50826d31 Update formatting of profile login buttons and display 2024-10-04 17:44:10 -04:00
21 changed files with 573 additions and 42 deletions

View File

@ -1,11 +1,14 @@
use crate::models::Artist; use crate::models::Artist;
use crate::components::dashboard_tile::DashboardTile; use crate::components::dashboard_tile::DashboardTile;
use serde::{Serialize, Deserialize};
use chrono::NaiveDate; use chrono::NaiveDate;
/// Holds information about an album /// Holds information about an album
/// ///
/// Intended to be used in the front-end /// Intended to be used in the front-end
#[derive(Serialize, Deserialize, Clone)]
pub struct AlbumData { pub struct AlbumData {
/// Album id /// Album id
pub id: i32, pub id: i32,

33
src/api/album.rs Normal file
View File

@ -0,0 +1,33 @@
use leptos::*;
use crate::albumdata::AlbumData;
use crate::songdata::SongData;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos::server_fn::error::NoCustomError;
use crate::database::get_db_conn;
}
}
#[server(endpoint = "album/get")]
pub async fn get_album(id: i32) -> Result<AlbumData, ServerFnError> {
use crate::models::Album;
let db_con = &mut get_db_conn();
let album = Album::get_album_data(id,db_con)
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting album: {}", e)))?;
Ok(album)
}
#[server(endpoint = "album/get_songs")]
pub async fn get_songs(id: i32) -> Result<Vec<SongData>, ServerFnError> {
use crate::models::Album;
use crate::auth::get_logged_in_user;
let user = get_logged_in_user().await?;
let db_con = &mut get_db_conn();
// TODO: NEEDS SONG DATA QUERIES
let songdata = Album::get_song_data(id,user,db_con)
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting song data: {}", e)))?;
Ok(songdata)
}

View File

@ -1,3 +1,4 @@
pub mod history; pub mod history;
pub mod profile; pub mod profile;
pub mod songs; pub mod songs;
pub mod album;

View File

@ -7,6 +7,7 @@ use leptos_router::*;
use crate::pages::login::*; use crate::pages::login::*;
use crate::pages::signup::*; use crate::pages::signup::*;
use crate::pages::profile::*; use crate::pages::profile::*;
use crate::pages::albumpage::*;
use crate::error_template::{AppError, ErrorTemplate}; use crate::error_template::{AppError, ErrorTemplate};
use crate::util::state::GlobalState; use crate::util::state::GlobalState;
@ -44,6 +45,7 @@ pub fn App() -> impl IntoView {
<Route path="search" view=Search /> <Route path="search" view=Search />
<Route path="user/:id" view=Profile /> <Route path="user/:id" view=Profile />
<Route path="user" view=Profile /> <Route path="user" view=Profile />
<Route path="album/:id" view=AlbumPage />
</Route> </Route>
<Route path="/login" view=Login /> <Route path="/login" view=Login />
<Route path="/signup" view=Signup /> <Route path="/signup" view=Signup />

View File

@ -87,6 +87,7 @@ pub async fn logout() -> Result<(), ServerFnError> {
auth_session.logout().await auth_session.logout().await
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {}", e)))?; .map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {}", e)))?;
leptos_axum::redirect("/login");
Ok(()) Ok(())
} }

View File

@ -8,3 +8,4 @@ pub mod upload;
pub mod song_list; pub mod song_list;
pub mod loading; pub mod loading;
pub mod error; pub mod error;
pub mod album_info;

View File

@ -0,0 +1,25 @@
use leptos::leptos_dom::*;
use leptos::*;
use crate::albumdata::AlbumData;
#[component]
pub fn AlbumInfo(albumdata: AlbumData) -> impl IntoView {
view! {
<div class="album-info">
<img class="album-image" src={albumdata.image_path} alt="dashboard-tile" />
<div class="album-body">
<p class="album-title">{albumdata.title}</p>
<div class="album-artists">
{
albumdata.artists.iter().map(|artist| {
view! {
<a class="album-artist" href={format!("/artist/{}", artist.id.unwrap())}>{artist.name.clone()}</a>
}
}).collect::<Vec<_>>()
}
</div>
</div>
</div>
}.into_view()
}

View File

@ -1,6 +1,8 @@
use leptos::leptos_dom::*; use leptos::leptos_dom::*;
use leptos::*; use leptos::*;
use leptos_icons::*; use leptos_icons::*;
use crate::auth::logout;
use crate::util::state::GlobalState;
#[component] #[component]
pub fn Personal() -> impl IntoView { pub fn Personal() -> impl IntoView {
@ -14,18 +16,66 @@ pub fn Personal() -> impl IntoView {
#[component] #[component]
pub fn Profile() -> impl IntoView { pub fn Profile() -> impl IntoView {
let (dropdown_open, set_dropdown_open) = create_signal(false); let (dropdown_open, set_dropdown_open) = create_signal(false);
let user = GlobalState::logged_in_user();
let open_dropdown = move |_| { let open_dropdown = move |_| {
set_dropdown_open.update(|value| *value = !*value); set_dropdown_open.update(|value| *value = !*value);
log!("opened dropdown");
}; };
let user_profile_picture = move || {
user.get().and_then(|user| {
if let Some(user) = user {
if user.id.is_none() {
return None;
}
Some(format!("/assets/images/profile/{}.webp", user.id.unwrap()))
} else {
None
}
})
};
view! { view! {
<div class="profile-container"> <div class="profile-container">
<div class="profile-name">
<Suspense
fallback=|| view!{
<h1>Not Logged In</h1>
}>
<Show
when=move || user.get().map(|user| user.is_some()).unwrap_or(false)
fallback=|| view!{
<h1>Not Logged In</h1>
}>
<h1>{move || user.get().map(|user| user.map(|user| user.username))}</h1>
</Show>
</Suspense>
</div>
<div class="profile-icon" on:click=open_dropdown> <div class="profile-icon" on:click=open_dropdown>
<Icon icon=icondata::CgProfile /> <Suspense fallback=|| view! { <Icon icon=icondata::CgProfile width="45" height="45"/> }>
<Show
when=move || user.get().map(|user| user.is_some()).unwrap_or(false)
fallback=|| view! { <Icon icon=icondata::CgProfile width="45" height="45"/> }
>
<object class="profile-image" data={user_profile_picture} type="image/webp">
<Icon class="profile-image" icon=icondata::CgProfile width="45" height="45"/>
</object>
</Show>
</Suspense>
</div> </div>
<div class="dropdown-container" style={move || if dropdown_open() {"display: flex"} else {"display: none"}}> <div class="dropdown-container" style={move || if dropdown_open() {"display: flex"} else {"display: none"}}>
<Suspense
fallback=|| view!{
<DropDownNotLoggedIn /> <DropDownNotLoggedIn />
}>
<Show
when=move || user.get().map(|user| user.is_some()).unwrap_or(false)
fallback=|| view!{
<DropDownNotLoggedIn />
}>
<DropDownLoggedIn />
</Show>
</Suspense>
</div> </div>
</div> </div>
} }
@ -33,10 +83,33 @@ pub fn Profile() -> impl IntoView {
#[component] #[component]
pub fn DropDownNotLoggedIn() -> impl IntoView { pub fn DropDownNotLoggedIn() -> impl IntoView {
view! { view! {
<div class="dropdown-not-logged"> <div class="dropdown-logged">
<h1>Not Logged in!</h1> <h1>Not Logged In</h1>
<a href="/login"><button class="auth-button">Log In</button></a> <a href="/login"><button class="auth-button">Log In</button></a>
<a href="/signup"><button class="auth-button">Sign up</button></a> <a href="/signup"><button class="auth-button">Sign Up</button></a>
</div>
}
}
#[component]
pub fn DropDownLoggedIn() -> impl IntoView {
let logout = move |_| {
spawn_local(async move {
let result = logout().await;
if let Err(err) = result {
log!("Error logging out: {:?}", err);
} else {
let user = GlobalState::logged_in_user();
user.refetch();
log!("Logged out successfully");
}
});
};
view! {
<div class="dropdown-logged">
<h1>"Logged In"</h1>
<button on:click=logout class="auth-button">Log Out</button>
</div> </div>
} }
} }

View File

@ -6,8 +6,10 @@ use cfg_if::cfg_if;
cfg_if! { cfg_if! {
if #[cfg(feature = "ssr")] { if #[cfg(feature = "ssr")] {
use diesel::prelude::*; use diesel::prelude::*;
use crate::database::PgPooledConn; use crate::database::*;
use std::error::Error; use std::error::Error;
use crate::songdata::SongData;
use crate::albumdata::AlbumData;
} }
} }
@ -498,7 +500,7 @@ impl Album {
Ok(()) Ok(())
} }
/// Get songs by this artist from the database /// Get songs by this album from the database
/// ///
/// The `id` field of this album must be present (Some) to get songs /// The `id` field of this album must be present (Some) to get songs
/// ///
@ -525,6 +527,153 @@ impl Album {
Ok(my_songs) Ok(my_songs)
} }
/// Obtain an album from its albumid
/// # Arguments
///
/// * `album_id` - The id of the album to select
/// * `conn` - A mutable reference to a database connection
///
/// # Returns
///
/// * `Result<Album, Box<dyn Error>>` - A result indicating success with the desired album, or an error
///
#[cfg(feature = "ssr")]
pub fn get_album_data(album_id: i32, conn: &mut PgPooledConn) -> Result<AlbumData, Box<dyn Error>> {
use crate::schema::*;
let album: Vec<(Album, std::option::Option<Artist>)> = albums::table
.find(album_id)
.left_join(songs::table.on(albums::id.nullable().eq(songs::album_id)))
.left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id)))
.select((
albums::all_columns,
artists::all_columns.nullable()
))
.distinct()
.load(conn)?;
let mut artist_list: Vec<Artist> = Vec::new();
for (_, artist) in album {
if let Some(artist) = artist {
artist_list.push(artist);
}
}
// Get info of album
let albuminfo = albums::table
.filter(albums::id.eq(album_id))
.first::<Album>(conn)?;
let img = albuminfo.image_path.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string());
let albumdata = AlbumData {
id: albuminfo.id.unwrap(),
title: albuminfo.title,
artists: artist_list,
release_date: albuminfo.release_date,
image_path: img
};
Ok(albumdata)
}
/// Obtain an album from its albumid
/// # Arguments
///
/// * `album_id` - The id of the album to select
/// * `conn` - A mutable reference to a database connection
///
/// # Returns
///
/// * `Result<Album, Box<dyn Error>>` - A result indicating success with the desired album, or an error
///
#[cfg(feature = "ssr")]
pub fn get_song_data(album_id: i32, user_like_dislike: Option<User>, conn: &mut PgPooledConn) -> Result<Vec<SongData>, Box<dyn Error>> {
use crate::schema::*;
use std::collections::HashMap;
let song_list = if let Some(user_like_dislike) = user_like_dislike {
let user_like_dislike_id = user_like_dislike.id.unwrap();
let song_list: Vec<(Album, Option<Song>, Option<Artist>, Option<(i32, i32)>, Option<(i32, i32)>)> =
albums::table
.find(album_id)
.left_join(songs::table.on(albums::id.nullable().eq(songs::album_id)))
.left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id)))
.left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(user_like_dislike_id))))
.left_join(song_dislikes::table.on(songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(user_like_dislike_id))))
.select((
albums::all_columns,
songs::all_columns.nullable(),
artists::all_columns.nullable(),
song_likes::all_columns.nullable(),
song_dislikes::all_columns.nullable()
))
.order(songs::track.asc())
.load(conn)?;
song_list
} else {
let song_list: Vec<(Album, Option<Song>, Option<Artist>)> =
albums::table
.find(album_id)
.left_join(songs::table.on(albums::id.nullable().eq(songs::album_id)))
.left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id)))
.select((
albums::all_columns,
songs::all_columns.nullable(),
artists::all_columns.nullable()
))
.order(songs::track.asc())
.load(conn)?;
let song_list: Vec<(Album, Option<Song>, Option<Artist>, Option<(i32, i32)>, Option<(i32, i32)>)> =
song_list.into_iter().map( |(album, song, artist)| (album, song, artist, None, None) ).collect();
song_list
};
let mut album_songs: HashMap<i32, SongData> = HashMap::with_capacity(song_list.len());
for (album, song, artist, like, dislike) in song_list {
if let Some(song) = song {
if let Some(stored_songdata) = album_songs.get_mut(&song.id.unwrap()) {
// If the song is already in the map, update the artists
if let Some(artist) = artist {
stored_songdata.artists.push(artist);
}
} else {
let like_dislike = match (like, dislike) {
(Some(_), Some(_)) => Some((true, true)),
(Some(_), None) => Some((true, false)),
(None, Some(_)) => Some((false, true)),
_ => None,
};
let image_path = song.image_path.unwrap_or(
album.image_path.clone().unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()));
let songdata = SongData {
id: song.id.unwrap(),
title: song.title,
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
album: Some(album),
track: song.track,
duration: song.duration,
release_date: song.release_date,
song_path: song.storage_path,
image_path: image_path,
like_dislike: like_dislike,
};
album_songs.insert(song.id.unwrap(), songdata);
}
}
}
// Sort the songs by date
let mut songdata: Vec<SongData> = album_songs.into_values().collect();
songdata.sort_by(|a, b| b.track.cmp(&a.track));
Ok(songdata)
}
} }
/// Model for a song /// Model for a song

View File

@ -1,3 +1,4 @@
pub mod login; pub mod login;
pub mod signup; pub mod signup;
pub mod profile; pub mod profile;
pub mod albumpage;

88
src/pages/albumpage.rs Normal file
View File

@ -0,0 +1,88 @@
use leptos::leptos_dom::*;
use leptos::*;
use leptos_router::*;
use crate::components::song_list::*;
use crate::api::album::*;
use crate::components::album_info::*;
#[derive(Params, PartialEq)]
struct AlbumParams {
id: i32
}
#[component]
pub fn AlbumPage() -> impl IntoView {
let params = use_params::<AlbumParams>();
let id = move || {params.with(|params| {
params.as_ref()
.map(|params| params.id)
.map_err(|e| e.clone())
})
};
let song_list = create_resource(
id,
|value| async move {
match value {
Ok(v) => {get_songs(v).await},
Err(e) => {Err(ServerFnError::Request(format!("Error getting song data: {}", e).into()))},
}
},
);
let albumdata = create_resource(
id,
|value| async move {
match value {
Ok(v) => {get_album(v).await},
Err(e) => {Err(ServerFnError::Request(format!("Error getting song data: {}", e).into()))},
}
},
);
view! {
<div class="album-page-container">
<div class="album-header">
<Suspense
fallback=move || view! { <p class="loading">"Loading..."</p> }
>
{move || {
albumdata.with( |albumdata| {
match albumdata {
Some(Ok(s)) => {
view! { <AlbumInfo albumdata=(*s).clone() /> }
},
Some(Err(e)) => {
view! { <div class="error">{format!("Error loading album : {}",e)}</div> }.into_view()
},
None => {view! { }.into_view()}
}
})
}}
</Suspense>
</div>
<div class="album-songs">
<Suspense
fallback=move || view! { <p class="loading">"Loading..."</p> }
>
{move || {
song_list.with( |song_list| {
match song_list {
Some(Ok(s)) => {
view! { <SongList songs=(*s).clone()/> }
},
Some(Err(e)) => {
view! { <div class="error">{format!("Error loading albums: : {}",e)}</div> }.into_view()
},
None => {view! { }.into_view()}
}
})
}}
</Suspense>
</div>
</div>
}
}

View File

@ -117,7 +117,7 @@ pub async fn validate_user(credentials: UserCredentials) -> Result<Option<User>,
/// Get a user from the database by username or email /// Get a user from the database by username or email
/// Returns a Result with the user if found, None if not found, or an error if there was a problem /// Returns a Result with the user if found, None if not found, or an error if there was a problem
#[server(endpoint = "get_user")] #[server(endpoint = "find_user")]
pub async fn get_user(username_or_email: String) -> Result<Option<User>, ServerFnError> { pub async fn get_user(username_or_email: String) -> Result<Option<User>, ServerFnError> {
let mut user = find_user(username_or_email).await?; let mut user = find_user(username_or_email).await?;

95
style/album_page.scss Normal file
View File

@ -0,0 +1,95 @@
@import 'theme.scss';
.album-page-container {
$album-header-height: calc(40% - 2px);
width: $center-viewport-width;
max-height: $center-viewport-height;
overflow: hidden;
margin-top:2px;
.album-header {
$album-header-padding: 20px;
height: $album-header-height;
width: calc(100% - 2*$album-header-padding);
margin: 2px;
padding: $album-header-padding;
background-image: linear-gradient($accent-color, $background-color);
border-radius: 15px;
.album-info {
width: 100%;
height: 100%;
}
}
.album-songs {
width: 100%;
max-height: calc(100% - $album-header-height);
overflow-x: hidden;
overflow-y: scroll;
}
}
.album-info {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
.album-image {
max-width: 80%;
max-height: 80%;
box-shadow: 10px 10px 50px -10px $background-color;
}
.album-body {
display: flex;
flex-flow: column nowrap;
justify-content: center;
.album-title {
color: $text-controls-color;
font-size: 40px;
font-weight: bold;
margin:15px;
text-align: center;
}
.album-artists {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
align-content: space-around;
margin:15px;
color: $text-controls-color;
font-size: 20px;
.album-artist {
margin: 5px;
text-align: center;
text-decoration: underline;
}
}
}
a {
color: $text-controls-color;
}
a:visited {
color: $text-controls-color;
}
a:hover {
color: $controls-hover-color;
}
a:active {
color: $controls-click-color;
}
}

View File

@ -1,6 +1,9 @@
@import "theme.scss"; @import "theme.scss";
.dashboard-container { .dashboard-container {
max-width: $center-viewport-width;
max-height: $center-viewport-height;
.dashboard-header { .dashboard-header {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 300; font-weight: 300;

View File

@ -1,17 +1,20 @@
@import "theme.scss"; @import "theme.scss";
.home-container { .home-container {
margin-top: 0; width: 100vw;
width: 100%;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.home-component { .home-component {
background: #1c1c1c; background: #1c1c1c;
width: calc(100% - 22rem - 16rem);
width: calc($center-viewport-width - 4px - 2rem);
height: calc($center-viewport-height - 4px - 1rem);
margin: 2px; margin: 2px;
padding: 0.2rem 1.5rem $playbar-size 1rem; padding: 0.5rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: scroll; overflow: scroll;
} }

View File

@ -15,6 +15,8 @@
@import 'song_list.scss'; @import 'song_list.scss';
@import 'profile.scss'; @import 'profile.scss';
@import 'loading.scss'; @import 'loading.scss';
@import 'album_page.scss';
@import 'scrollbar.scss';
body { body {
font-family: sans-serif; font-family: sans-serif;

View File

@ -1,9 +1,9 @@
@import "theme.scss"; @import "theme.scss";
.personal-container { .personal-container {
width: 16rem; width: $personal-width;
background: #1c1c1c; background: #1c1c1c;
margin: 2px; margin: $personal-margin;
border-radius: 0.5rem; border-radius: 0.5rem;
.profile-container { .profile-container {
@ -11,9 +11,19 @@
border-radius: 0.4rem; border-radius: 0.4rem;
margin: 0.2rem; margin: 0.2rem;
min-height: 6rem; min-height: 6rem;
border: 2px solid rgba(89, 89, 89, 0.199); border: 0.2rem solid rgba(89, 89, 89, 0.199);
padding: 0.5rem; padding: 0.5rem;
.profile-name {
display: flex;
flex-direction: column;
margin-left: 0.5rem;
h1 {
font-size: 1.2rem;
margin: 0;
}
}
.profile-icon { .profile-icon {
display: inline-flex; display: inline-flex;
padding: 0.2rem; padding: 0.2rem;
@ -21,17 +31,23 @@
font-size: 2rem; font-size: 2rem;
border-radius: 50%; border-radius: 50%;
transition: all 0.3s; transition: all 0.3s;
height: max-content; height: 45;
width: 45;
margin-left: auto; margin-left: auto;
.profile-image {
width: 100%;
height: 100%;
border-radius: 50%;
}
} }
.profile-icon:hover { .profile-icon:hover {
transform: scale(1.1); transform: scale(1.05);
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.profile-icon:active { .profile-icon:active {
transform: scale(0.8); transform: scale(0.95);
} }
.dropdown-container { .dropdown-container {
position: absolute; position: absolute;
@ -41,10 +57,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
width: 10rem; width: 10rem;
z-index: 1; z-index: 1;
background-color: red; background-color: #1c1c1c;
border: 1px solid grey; border: 0.2rem solid rgba(89, 89, 89, 0.199);
.dropdown-not-logged { .dropdown-logged {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@ -53,21 +69,29 @@
h1 { h1 {
font-size: 1.2rem; font-size: 1.2rem;
} }
.profile-info {
display: flex;
width: 100%;
justify-content: center;
border-top: 0.2rem solid rgba(89, 89, 89, 0.199);
border-bottom: 0.2rem solid rgba(89, 89, 89, 0.199);
h1 {
font-size: 1rem;
margin-top: 0.5rem;
}
}
.auth-button { .auth-button {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: #1c1c1c;
border: 0.2rem solid rgba(89, 89, 89, 0.199);
color: white;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 0.5rem;
} }
} }
} }
.dropdown-container:before {
content: "";
position: absolute;
top: -0.4rem;
right: 0.92rem;
width: 10px;
height: 10px;
transform: rotate(45deg);
background-color: red;
border-left: 1px solid grey;
border-top: 1px solid grey;
}
} }
} }

12
style/scrollbar.scss Normal file
View File

@ -0,0 +1,12 @@
::-webkit-scrollbar {
width: 10px;
background-blend-mode: $scrollbar-background-color;
}
::-webkit-scrollbar-thumb {
background: $scrollbar-handle-color;
}
::-webkit-scrollbar-thumb:hover {
background: $scrollbar-handle-hover-color;
}

View File

@ -2,13 +2,15 @@
.sidebar-container { .sidebar-container {
background-color: transparent; background-color: transparent;
width: 22rem; width: $sidebar-width;
height: calc(100% - 75px); height: $center-viewport-height;
margin: 2px;
.sidebar-top-container { .sidebar-top-container {
border-radius: 1rem; border-radius: 1rem;
background-color: #1c1c1c; background-color: #1c1c1c;
height: 9rem; height: 9rem;
margin: 3px; margin: 2px 0px;
padding: 0.1rem 1rem 1rem 1rem; padding: 0.1rem 1rem 1rem 1rem;
position: relative; position: relative;
.header { .header {
@ -68,7 +70,7 @@
.sidebar-bottom-container { .sidebar-bottom-container {
border-radius: 1rem; border-radius: 1rem;
background-color: #1c1c1c; background-color: #1c1c1c;
margin: 3px; margin: 2px 0px;
margin-top: 6px; margin-top: 6px;
padding: 0.2rem 1rem 1rem 1rem; padding: 0.2rem 1rem 1rem 1rem;
height: calc(100% - 9rem); height: calc(100% - 9rem);

View File

@ -10,10 +10,23 @@ $controls-click-color: #909090;
$play-bar-background-color: #212121; $play-bar-background-color: #212121;
$play-grad-start: #0a0533; $play-grad-start: #0a0533;
$play-grad-end: $accent-color; $play-grad-end: $accent-color;
$border-color: #7851ed;
$queue-background-color: $play-bar-background-color; $queue-background-color: $play-bar-background-color;
$scrollbar-background-color: #101010;
$scrollbar-handle-color:#292929;
$scrollbar-handle-hover-color:#5c5b5b;
$auth-inputs: #796dd4; $auth-inputs: #796dd4;
$auth-containers: white; $auth-containers: white;
$dashboard-tile-size: 200px; $dashboard-tile-size: 200px;
$playbar-size: 75px; $playbar-size: 75px;
$personal-width: 16rem;
$sidebar-width: 22rem;
$personal-margin: 2px;
$sidebar-margin: 2px;
$center-viewport-width: calc(100vw - $personal-width - $sidebar-width - 2*$personal-margin - 2*$sidebar-margin);
$center-viewport-height: calc(100vh - $playbar-size);