Better login modal, post layout fixing

This commit is contained in:
Imbus 2023-11-13 09:34:32 +01:00
parent e6bf6bc8a6
commit 8d23f17f76
9 changed files with 269 additions and 101 deletions

View file

@ -0,0 +1,17 @@
export function Arrow() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
</svg>
);
}

View file

@ -0,0 +1,67 @@
import { onCleanup, useContext } from "solid-js";
import { ModalContext } from "./Root";
import { LoginForm } from "./RegLogin/Login";
import { RegisterForm } from "./RegLogin/Register";
export function LoginModal() {
const modal_ctx = useContext(ModalContext);
if (!modal_ctx) {
// Handle the case where ModalContext is not available
return null;
}
const closeModal = () => {
modal_ctx.setLoginModalOpen(false);
};
// Close the modal when the component is unmounted
onCleanup(() => {
modal_ctx.setLoginModalOpen(false);
});
return (
<>
{modal_ctx.loginModalOpen() && (
<dialog class="modal modal-open">
<div class="modal-box">
<form method="dialog">
<button
onclick={closeModal}
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
>
</button>
</form>
<h3 class="font-bold text-lg mb-2">Welcome!</h3>
<div class="tabs tabs-lifted">
<input
type="radio"
name="login_tabs"
class="tab"
aria-label="Login"
checked
/>
<div class="tab-content bg-base-100 border-base-300 rounded-box p-10">
<LoginForm />
</div>
<input
type="radio"
name="login_tabs"
class="tab"
aria-label="Register"
/>
<div class="tab-content bg-base-100 border-base-300 rounded-box p-10">
<RegisterForm />
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={closeModal}>close</button>
</form>
</dialog>
)}
</>
);
}

View file

@ -2,26 +2,23 @@ import { useContext } from "solid-js";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { LoginContext } from "./Root"; import { LoginContext } from "./Root";
import { ModalContext } from "./Root"; import { ModalContext } from "./Root";
import { LoginForm } from "./Login";
function MenuItem(props: { href: string; children: any }) {
return (
<li>
<A class="justify-center" href={props.href} end>
{props.children}
</A>
</li>
);
}
function Menu() { function Menu() {
let login_ctx = useContext(LoginContext); let login_ctx = useContext(LoginContext);
return ( return (
<ul class="menu menu-horizontal bg-base-100 rounded-box space-x-2"> <ul class="menu md:menu-horizontal rounded-box">
<li> <MenuItem href="/">Home</MenuItem>
<A href="/" end> {login_ctx?.token() != "" && <MenuItem href="/new">New Post</MenuItem>}
Home
</A>
</li>
{login_ctx?.token() != "" ? (
<li>
<A href="/new" end>
New
</A>
</li>
) : (
<></>
)}
</ul> </ul>
); );
} }
@ -30,61 +27,42 @@ export function Navbar() {
let modal_ctx = useContext(ModalContext); let modal_ctx = useContext(ModalContext);
let login_ctx = useContext(LoginContext); let login_ctx = useContext(LoginContext);
const logout = () => {
localStorage.setItem("token", "");
localStorage.setItem("username", "");
login_ctx?.setToken("");
login_ctx?.setUsername("");
};
// Capitalize the first letter of the username
const username =
login_ctx && login_ctx.username()
? login_ctx.username().charAt(0).toUpperCase() +
login_ctx.username().slice(1)
: "Guest";
return ( return (
<div class="navbar text-neutral-content max-w-3xl max-w"> <div class="navbar text-neutral-content max-w-3xl max-w rounded-box my-4">
<div class="flex-1"> <div class="flex-1">
<A href={"/"} class="btn btn-ghost normal-case text-xl"> <A href={"/"} class="btn btn-ghost normal-case text-xl">
FrostByte FrostByte
</A> </A>
</div> </div>
<div class="hidden md:flex"> <div class="flex">
<Menu /> <Menu />
</div> </div>
<div class="flex-1 justify-end hidden md:flex"> <div class="flex-1 justify-end flex">
<A <A
href="#" href="#"
class="btn btn-ghost normal-case text-sm" class="btn btn-ghost normal-case text-sm"
onClick={() => { onClick={() => {
if (login_ctx?.token() != "") { if (login_ctx?.token() != "") logout();
localStorage.setItem("token", ""); else modal_ctx?.setLoginModalOpen(true);
localStorage.setItem("username", "");
login_ctx?.setToken("");
login_ctx?.setUsername("");
return;
}
modal_ctx?.setLoginModalOpen(true);
}} }}
> >
{login_ctx?.token() != "" ? login_ctx?.username() : "Login"} {login_ctx?.token() != "" ? username : "Login"}
</A> </A>
</div> </div>
</div> </div>
); );
} }
// This is a modal
export function Login() {
const modal_ctx = useContext(ModalContext);
return (
<dialog id="login_modal" class="modal" open={modal_ctx?.loginModalOpen()}>
<div class="modal-box">
<h3 class="font-bold text-lg">Hello!</h3>
<p class="py-4">Login to your FrostByte account.</p>
<LoginForm />
</div>
<form
method="dialog"
// This backdrop renders choppy on my machine. Likely because of the blur filter or misuse of css transisions
class="modal-backdrop backdrop-brightness-50 backdrop-blur-sm backdrop-contrast-100 transition-all transition-300"
onsubmit={(e) => {
// This is just needed to set the state to false
// The modal will close itself without this code, but without setting the state
e.preventDefault();
modal_ctx?.setLoginModalOpen(false);
}}
>
<button>close</button>
</form>
</dialog>
);
}

View file

@ -2,6 +2,7 @@ import { createSignal } from "solid-js";
import { getPosts } from "./api"; import { getPosts } from "./api";
import { Post } from "./api"; import { Post } from "./api";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { Arrow } from "./Icons";
export function Posts() { export function Posts() {
const [posts, setPosts] = createSignal([] as Post[]); const [posts, setPosts] = createSignal([] as Post[]);
@ -13,17 +14,15 @@ export function Posts() {
}); });
return ( return (
<div class="flex flex-col space-y-2 w-full md:w-96"> <>
{loading() ? ( {loading() && (
<span class="loading loading-spinner loading-lg self-center"></span> <span class="loading loading-spinner loading-lg self-center"></span>
) : (
<></>
)} )}
{posts().map((post) => { {posts().map((post) => {
if (post.content == "") return; // Filtering out empty posts, remove this later if (post.content == "") return; // Filtering out empty posts, remove this later
return <PostSegment post={post}></PostSegment>; return <PostSegment post={post}></PostSegment>;
})} })}
</div> </>
); );
} }
@ -31,13 +30,17 @@ export function Posts() {
export function PostSegment({ post }: { post: Post }) { export function PostSegment({ post }: { post: Post }) {
const nav = useNavigate(); const nav = useNavigate();
return ( return (
<div <div class="card border-b-2 flex-grow border-b-base-300 bg-base-200 hover:bg-base-300 compact text-base-content w-full">
onClick={() => nav("/post/" + post?.id)}
class="card bg-base-200 shadow-lg compact text-base-content w-full"
>
<div class="card-body"> <div class="card-body">
<p class="text-base-content break-words">{post?.content}</p> <p class="text-base-content break-words">{post?.content}</p>
<div class="card-actions justify-end">
<button onClick={() => nav("/post/" + post?.id)} class="btn btn-xs">
<Arrow />
</button>
</div>
</div> </div>
</div> </div>
); );
} }

View file

@ -1,5 +1,5 @@
import { createSignal, useContext } from "solid-js"; import { createSignal, useContext } from "solid-js";
import { LoginContext, ModalContext } from "./Root"; import { LoginContext, ModalContext } from "../Root";
export function LoginForm() { export function LoginForm() {
const modal_ctx = useContext(ModalContext); const modal_ctx = useContext(ModalContext);
@ -17,49 +17,49 @@ export function LoginForm() {
}, 1000); }, 1000);
} }
return ( async function loginPress(e: Event) {
<form class="form-control"> e.preventDefault();
<label class="label">
<span class="label-text">Username</span>
</label>
<input
type="text"
placeholder="username"
value={username()}
class="input input-bordered"
onChange={(e) => {
setUsername(e.target.value);
}} />
<label class="label">
<span class="label-text">Password</span>
</label>
<input
type="password"
placeholder="password"
value={password()}
class="input input-bordered"
onChange={(e) => {
setPassword(e.target.value);
}} />
<button
class={"btn btn-primary mt-4" + (error() ? " btn-error" : "")}
onClick={(b) => {
b.preventDefault();
setWaiting(true); setWaiting(true);
submitLogin(username(), password()).then((token) => { submitLogin(username(), password()).then((token) => {
if (token != "") { if (token != "") {
setWaiting(false); setWaiting(false);
setError(false); setError(false);
login_ctx?.setUsername(username()); login_ctx?.setUsername(username());
setUsername("");
setPassword("");
login_ctx?.setToken(token); login_ctx?.setToken(token);
modal_ctx?.setLoginModalOpen(false); modal_ctx?.setLoginModalOpen(false);
setUsername("");
setPassword("");
} else { } else {
loginFailed(); loginFailed();
} }
}); });
}
return (
<form class="form-control space-y-2">
<input
type="text"
placeholder="Username"
value={username()}
class="input input-bordered"
onChange={(e) => {
setUsername(e.target.value);
}} }}
/>
<input
type="password"
placeholder="Password"
value={password()}
class="input input-bordered"
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<button
class={"btn btn-primary" + (error() ? " btn-error" : "")}
onClick={ loginPress }
> >
{waiting() ? "Logging in..." : "Login"} {waiting() ? "Logging in..." : "Login"}
</button> </button>
@ -73,6 +73,7 @@ export async function submitLogin(
username: string, username: string,
password: string password: string
): Promise<string> { ): Promise<string> {
if(username == "" || password == "") return "";
const response = await fetch("/api/login", { const response = await fetch("/api/login", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -89,4 +90,3 @@ export async function submitLogin(
} }
return ""; return "";
} }

View file

@ -0,0 +1,103 @@
import { createSignal, useContext } from "solid-js";
import { LoginContext, ModalContext } from "../Root";
export function RegisterForm() {
const modal_ctx = useContext(ModalContext);
const login_ctx = useContext(LoginContext);
const [username, setUsername] = createSignal<string>("");
const [password, setPassword] = createSignal<string>("");
const [captcha, setCaptcha] = createSignal<string>("");
const [waiting, setWaiting] = createSignal(false);
const [error, setError] = createSignal(false);
async function loginFailed() {
setError(true);
setWaiting(false);
setTimeout(() => {
setError(false);
}, 1000);
}
async function regPress(e: Event) {
e.preventDefault();
setWaiting(true);
submitRegistration(username(), password(), captcha()).then((token) => {
if (token != "") {
setWaiting(false);
setError(false);
login_ctx?.setUsername(username());
login_ctx?.setToken(token);
modal_ctx?.setLoginModalOpen(false);
setUsername("");
setPassword("");
} else {
loginFailed();
}
});
}
return (
<form class="form-control space-y-2">
<input
type="text"
placeholder="Username"
value={username()}
class="input input-bordered"
onChange={(e) => {
setUsername(e.target.value);
}}
/>
<input
type="password"
placeholder="Password"
value={password()}
class="input input-bordered"
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<input
type="text"
placeholder="Captcha"
value={password()}
class="input input-bordered"
onChange={(e) => {
setCaptcha(e.target.value);
}}
/>
<button
class={"btn btn-primary" + (error() ? " btn-error" : "")}
onClick={regPress}
>
{waiting() ? "Logging in..." : "Login"}
</button>
</form>
);
}
// This function is responsible for sending the login request to the server
// and storing the token in localstorage
export async function submitRegistration(
username: string,
password: string,
captcha: string
): Promise<string> {
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, captcha }),
});
if (response.ok) {
const data = await response.json();
if (data.token && data.username) {
localStorage.setItem("token", data.token);
localStorage.setItem("username", data.username);
return data.token;
}
}
return "";
}

View file

@ -5,7 +5,8 @@ import { createPost } from "./api";
import { NewPost } from "./api"; import { NewPost } from "./api";
import { Navbar } from "./Navbar"; import { Navbar } from "./Navbar";
import { Primary } from "./Primary"; import { Primary } from "./Primary";
import { Login } from "./Navbar"; // import { Login } from "./Navbar";
import { LoginModal } from "./LoginModal";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
// Representing the state of varoious modals. // Representing the state of varoious modals.
@ -44,9 +45,9 @@ function Root() {
<LoginContext.Provider <LoginContext.Provider
value={{ token, setToken, username, setUsername }} value={{ token, setToken, username, setUsername }}
> >
<div class="flex flex-col items-center my-2"> <div class="flex flex-col items-center">
<Navbar /> <Navbar />
<Login /> <LoginModal />
<div class="flex flex-col items-center md:w-96 space-y-2"> <div class="flex flex-col items-center md:w-96 space-y-2">
<Primary /> <Primary />
</div> </div>
@ -66,10 +67,12 @@ export function NewPostInputArea() {
const sendPost = () => { const sendPost = () => {
setWaiting(true); setWaiting(true);
const response = createPost({ const response = createPost({
content: content(), content: content(),
token: login_ctx?.token(), token: login_ctx?.token(),
} as NewPost); } as NewPost);
if (response) { if (response) {
response.then(() => { response.then(() => {
setWaiting(false); setWaiting(false);

View file

@ -9,11 +9,9 @@ export function SinglePost() {
return ( return (
<Suspense fallback={<div>Some loading message</div>}> <Suspense fallback={<div>Some loading message</div>}>
<div>
<Show when={post()}> <Show when={post()}>
<PostSegment post={post()!}></PostSegment> <PostSegment post={post()!}></PostSegment>
</Show> </Show>
</div>
</Suspense> </Suspense>
); );
} }

View file

@ -29,7 +29,6 @@ export async function getPost(id: string): Promise<Post> {
} }
export async function createPost(post: NewPost): Promise<void> { export async function createPost(post: NewPost): Promise<void> {
// await fetch(`${API_URL}`, {
await fetch("/api/posts", { await fetch("/api/posts", {
method: "POST", method: "POST",
headers: { headers: {