diff --git a/apps/api/src/spotify/app.ts b/apps/api/src/spotify/app.ts --- a/apps/api/src/spotify/app.ts +++ b/apps/api/src/spotify/app.ts @@ -72,7 +72,18 @@ const state = crypto.randomBytes(16).toString("hex"); ctx.kv.set(state, did); - const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${spotifyAccount?.spotify_apps?.spotifyAppId}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=user-read-private%20user-read-email%20user-read-playback-state%20user-read-currently-playing%20user-modify-playback-state%20playlist-modify-public%20playlist-modify-private%20playlist-read-private%20playlist-read-collaborative&state=${state}`; + const scopes = [ + "user-read-private", + "user-read-email", + "user-read-playback-state", + "user-read-currently-playing", + "user-modify-playback-state", + "playlist-modify-public", + "playlist-modify-private", + "playlist-read-private", + "playlist-read-collaborative", + ]; + const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${spotifyAccount?.spotify_apps?.spotifyAppId}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=${scopes.join("%20")}&state=${state}`; c.header( "Set-Cookie", `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`, diff --git a/apps/web/src/atoms/drawer.ts b/apps/web/src/atoms/drawer.ts new file mode 100644 --- /dev/null +++ b/apps/web/src/atoms/drawer.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const displayDrawerAtom = atom(false); diff --git a/apps/web/src/layouts/Links.tsx b/apps/web/src/layouts/Links.tsx new file mode 100644 --- /dev/null +++ b/apps/web/src/layouts/Links.tsx @@ -0,0 +1,63 @@ +import styled from "@emotion/styled"; + +const Link = styled.a` + text-decoration: none; + cursor: pointer; + display: block; + font-size: 13px; + + &:hover { + text-decoration: underline; + } +`; + +function Links() { + return ( +
+ + About + + + FAQ + + + API Docs + + + CLI + + + Source + + + Discord + +
+ ); +} + +export default Links; diff --git a/apps/web/src/layouts/LoginForm.tsx b/apps/web/src/layouts/LoginForm.tsx new file mode 100644 --- /dev/null +++ b/apps/web/src/layouts/LoginForm.tsx @@ -0,0 +1,178 @@ +import React from "react"; +import { Input } from "baseui/input"; +import { LabelMedium } from "baseui/typography"; +import { IconEye, IconEyeOff, IconLock } from "@tabler/icons-react"; +import { Button } from "baseui/button"; + +interface LoginFormProps { + handle: string; + setHandle: (value: string) => void; + password: string; + setPassword: (value: string) => void; + passwordLogin: boolean; + setPasswordLogin: (value: boolean) => void; + onLogin: () => void; + onCreateAccount: () => void; +} + +const LoginForm: React.FC = ({ + handle, + setHandle, + password, + setPassword, + passwordLogin, + setPasswordLogin, + onLogin, + onCreateAccount, +}) => { + return ( +
+
+
+ + Handle + + setPasswordLogin(!passwordLogin)} + > + {passwordLogin ? "OAuth Login" : "Password Login"} + +
+ + @ +
+ } + placeholder=".bsky.social" + value={handle} + onChange={(e) => setHandle(e.target.value)} + overrides={{ + Root: { + style: { + backgroundColor: "var(--color-input-background)", + borderColor: "var(--color-input-background)", + }, + }, + StartEnhancer: { + style: { + backgroundColor: "var(--color-input-background)", + }, + }, + InputContainer: { + style: { + backgroundColor: "var(--color-input-background)", + }, + }, + Input: { + style: { + color: "var(--color-text)", + caretColor: "var(--color-text)", + }, + }, + }} + /> + {passwordLogin && ( + + +
+ } + type="password" + placeholder="Password" + value={password} + onChange={(e) => setPassword(e.target.value)} + overrides={{ + Root: { + style: { + backgroundColor: "var(--color-input-background)", + borderColor: "var(--color-input-background)", + marginTop: "1rem", + }, + }, + StartEnhancer: { + style: { + backgroundColor: "var(--color-input-background)", + }, + }, + InputContainer: { + style: { + backgroundColor: "var(--color-input-background)", + }, + }, + Input: { + style: { + color: "var(--color-text)", + caretColor: "var(--color-text)", + }, + }, + MaskToggleHideIcon: { + component: () => ( + + ), + }, + MaskToggleShowIcon: { + component: () => ( + + ), + }, + }} + /> + )} + + + + Don't have an atproto handle yet? + +
+ You can create one at{" "} + + selfhosted.social + + ,{" "} + + Bluesky + {" "} + or any other AT Protocol service. +
+ + ); +}; + +export default LoginForm; diff --git a/apps/web/src/layouts/Main.tsx b/apps/web/src/layouts/Main.tsx --- a/apps/web/src/layouts/Main.tsx +++ b/apps/web/src/layouts/Main.tsx @@ -1,9 +1,6 @@ import styled from "@emotion/styled"; import { useSearch } from "@tanstack/react-router"; -import { Button } from "baseui/button"; -import { Input } from "baseui/input"; import { PLACEMENT, ToasterContainer } from "baseui/toast"; -import { LabelMedium } from "baseui/typography"; import { useAtomValue } from "jotai"; import { useEffect, useState } from "react"; import { profileAtom } from "../atoms/profile"; @@ -12,12 +9,11 @@ import { API_URL } from "../consts"; import useProfile from "../hooks/useProfile"; import CloudDrive from "./CloudDrive"; -import ExternalLinks from "./ExternalLinks"; import Navbar from "./Navbar"; import Search from "./Search"; import SpotifyLogin from "./SpotifyLogin"; -import { IconEye, IconEyeOff, IconLock } from "@tabler/icons-react"; import { consola } from "consola"; +import { displayDrawerAtom } from "../atoms/drawer"; const Container = styled.div` display: flex; @@ -41,14 +37,13 @@ } `; -const Link = styled.a` - text-decoration: none; - cursor: pointer; - display: block; - font-size: 13px; +const Drawer = styled.div` + @media (max-width: 1152px) { + display: block; + } - &:hover { - text-decoration: underline; + @media (min-width: 1153px) { + display: none; } `; @@ -57,8 +52,13 @@ withRightPane?: boolean; }; +import LoginForm from "./LoginForm"; +import ExternalLinks from "./ExternalLinks"; +import Links from "./Links"; + function Main(props: MainProps) { const { children } = props; + const displayDrawer = useAtomValue(displayDrawerAtom); const withRightPane = props.withRightPane ?? true; const [handle, setHandle] = useState(""); const [password, setPassword] = useState(""); @@ -186,15 +186,38 @@ }, }} /> + + - -
- {children} -
+ {!displayDrawer &&
{children}
} + {displayDrawer && ( + +
+ {jwt && profile && ( +
+ +
+ )} + {jwt && profile && !profile.spotifyConnected && } + {jwt && profile && } + {!jwt && ( +
+ +
+ )} + +
+
+ )}
{withRightPane && ( @@ -206,149 +229,16 @@ {jwt && profile && } {!jwt && (
-
-
- - Handle - - setPasswordLogin(!passwordLogin)} - > - {passwordLogin ? "OAuth Login" : "Password Login"} - -
- - @ -
- } - placeholder=".bsky.social" - value={handle} - onChange={(e) => setHandle(e.target.value)} - overrides={{ - Root: { - style: { - backgroundColor: "var(--color-input-background)", - borderColor: "var(--color-input-background)", - }, - }, - StartEnhancer: { - style: { - backgroundColor: "var(--color-input-background)", - }, - }, - InputContainer: { - style: { - backgroundColor: "var(--color-input-background)", - }, - }, - Input: { - style: { - color: "var(--color-text)", - caretColor: "var(--color-text)", - }, - }, - }} - /> - {passwordLogin && ( - - -
- } - type="password" - placeholder="Password" - value={password} - onChange={(e) => setPassword(e.target.value)} - overrides={{ - Root: { - style: { - backgroundColor: "var(--color-input-background)", - borderColor: "var(--color-input-background)", - marginTop: "1rem", - }, - }, - StartEnhancer: { - style: { - backgroundColor: "var(--color-input-background)", - }, - }, - InputContainer: { - style: { - backgroundColor: "var(--color-input-background)", - }, - }, - Input: { - style: { - color: "var(--color-text)", - caretColor: "var(--color-text)", - }, - }, - MaskToggleHideIcon: { - component: () => ( - - ), - }, - MaskToggleShowIcon: { - component: () => ( - - ), - }, - }} - /> - )} - - - - Don't have an atproto handle yet? - -
- You can create one at{" "} - - selfhosted.social - - ,{" "} - - Bluesky - {" "} - or any other AT Protocol service. -
+ )} @@ -356,50 +246,7 @@ -
- - About - - - FAQ - - - API Docs - - - CLI - - - Source - - - Discord - -
+
)} diff --git a/apps/web/src/layouts/Navbar/Navbar.tsx b/apps/web/src/layouts/Navbar/Navbar.tsx --- a/apps/web/src/layouts/Navbar/Navbar.tsx +++ b/apps/web/src/layouts/Navbar/Navbar.tsx @@ -21,7 +21,8 @@ import { useProfileStatsByDidQuery } from "../../hooks/useProfile"; import LogoDark from "../../assets/rocksky-logo-dark.png"; import LogoLight from "../../assets/rocksky-logo-light.png"; -import { IconUser } from "@tabler/icons-react"; +import { IconUser, IconMenu2, IconX } from "@tabler/icons-react"; +import { displayDrawerAtom } from "../../atoms/drawer"; const Container = styled.div` position: fixed; @@ -35,7 +36,18 @@ @media (max-width: 1152px) { width: 100%; + max-width: 770px; padding: 0 20px; + } +`; + +export const Menu = styled.div` + @media (max-width: 1152px) { + display: block; + } + + @media (min-width: 1153px) { + display: none; } `; @@ -69,6 +81,7 @@ `; function Navbar() { + const [displayDrawer, setDisplayDrawer] = useAtom(displayDrawerAtom); const [isOpen, setIsOpen] = useState(false); const [{ darkMode }, setTheme] = useAtom(themeAtom); const setProfile = useSetAtom(profileAtom); @@ -155,6 +168,27 @@ + + + {profile && jwt && (