2023-10-21 07:54:47 +02:00
|
|
|
import { Accessor, createSignal, useContext } from "solid-js";
|
2023-10-19 02:42:37 +02:00
|
|
|
import { createContext } from "solid-js";
|
|
|
|
|
2023-10-19 08:11:52 +02:00
|
|
|
import { Route, Routes, A } from "@solidjs/router";
|
|
|
|
|
|
|
|
import { createPost, getPosts } from "./api";
|
2023-10-19 02:42:37 +02:00
|
|
|
import { Post, NewPost } from "./api";
|
|
|
|
|
2023-10-21 07:54:47 +02:00
|
|
|
// Representing the state of varoious modals.
|
|
|
|
// So far we only have one modal, but we can add more later
|
|
|
|
// by adding more fields to this interface, or maybe an enum
|
|
|
|
interface ModalContextType {
|
|
|
|
loginModalOpen: Accessor<boolean>;
|
|
|
|
setLoginModalOpen: (value: boolean) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface LoginContextType {
|
|
|
|
token: Accessor<string>;
|
|
|
|
setToken: (value: string) => void;
|
|
|
|
username: Accessor<string>;
|
|
|
|
setUsername: (value: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
// It is unclear to me if this is the idiomatic way to do this in Solid
|
|
|
|
export const ModalContext = createContext<ModalContextType>();
|
|
|
|
export const LoginContext = createContext<LoginContextType>();
|
2023-10-19 02:42:37 +02:00
|
|
|
|
|
|
|
function Root() {
|
2023-10-21 07:54:47 +02:00
|
|
|
// All of these are passed into context providers
|
|
|
|
const [loginModalOpen, setLoginModalOpen] = createSignal(false);
|
|
|
|
const [token, setToken] = createSignal("");
|
|
|
|
const [username, setUsername] = createSignal("");
|
|
|
|
|
|
|
|
// This may not be the best place to do this.
|
|
|
|
localStorage.getItem("token") && setToken(localStorage.getItem("token")!);
|
|
|
|
localStorage.getItem("username") &&
|
|
|
|
setUsername(localStorage.getItem("username")!);
|
|
|
|
|
2023-10-19 02:42:37 +02:00
|
|
|
return (
|
|
|
|
<>
|
2023-10-21 07:54:47 +02:00
|
|
|
<ModalContext.Provider value={{ loginModalOpen, setLoginModalOpen }}>
|
|
|
|
<LoginContext.Provider
|
|
|
|
value={{ token, setToken, username, setUsername }}
|
|
|
|
>
|
|
|
|
<div class="flex flex-col items-center my-2">
|
|
|
|
<Navbar />
|
|
|
|
<Login />
|
|
|
|
<div class="flex flex-col items-center md:w-96 space-y-2">
|
|
|
|
<Primary />
|
|
|
|
</div>
|
2023-10-19 08:11:52 +02:00
|
|
|
</div>
|
2023-10-21 07:54:47 +02:00
|
|
|
</LoginContext.Provider>
|
|
|
|
</ModalContext.Provider>
|
2023-10-19 02:42:37 +02:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-19 08:11:52 +02:00
|
|
|
function Navbar() {
|
2023-10-21 07:54:47 +02:00
|
|
|
let modal_ctx = useContext(ModalContext);
|
|
|
|
let login_ctx = useContext(LoginContext);
|
2023-10-19 08:11:52 +02:00
|
|
|
return (
|
|
|
|
<div class="navbar bg-base-100 max-w-3xl max-w flex justify-evenly">
|
2023-10-21 01:55:47 +02:00
|
|
|
<a class="btn btn-ghost normal-case text-xl">FrostByte</a>
|
2023-10-19 08:11:52 +02:00
|
|
|
<Menu />
|
2023-10-21 07:54:47 +02:00
|
|
|
<A
|
|
|
|
href="#"
|
|
|
|
class="btn btn-ghost normal-case text-sm"
|
|
|
|
onClick={(b) => {
|
|
|
|
b.preventDefault();
|
|
|
|
modal_ctx?.setLoginModalOpen(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{login_ctx?.token() != "" ? login_ctx?.username() : "Login"}
|
2023-10-19 08:11:52 +02:00
|
|
|
</A>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-19 02:42:37 +02:00
|
|
|
function Menu() {
|
|
|
|
return (
|
2023-10-19 08:11:52 +02:00
|
|
|
<ul class="menu menu-horizontal bg-base-200 rounded-box space-x-2 justify-end">
|
|
|
|
<li>
|
|
|
|
<A href="/" end>
|
|
|
|
Home
|
|
|
|
</A>
|
|
|
|
</li>
|
2023-10-19 02:42:37 +02:00
|
|
|
<li>
|
2023-10-19 08:11:52 +02:00
|
|
|
<A href="/new" end>
|
|
|
|
New
|
|
|
|
</A>
|
2023-10-19 02:42:37 +02:00
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function NewPostInputArea() {
|
|
|
|
const [content, setContent] = createSignal("");
|
2023-10-21 07:54:47 +02:00
|
|
|
const login_ctx = useContext(LoginContext);
|
|
|
|
|
2023-10-19 02:42:37 +02:00
|
|
|
return (
|
2023-10-19 08:11:52 +02:00
|
|
|
<div class="flex flex-col space-y-2">
|
2023-10-19 02:42:37 +02:00
|
|
|
<textarea
|
|
|
|
class="textarea textarea-bordered"
|
2023-10-19 08:11:52 +02:00
|
|
|
placeholder="Speak your mind..."
|
2023-10-21 07:54:47 +02:00
|
|
|
maxLength={500}
|
2023-10-19 02:42:37 +02:00
|
|
|
oninput={(input) => {
|
|
|
|
setContent(input.target.value);
|
|
|
|
}}
|
|
|
|
></textarea>
|
2023-10-19 08:11:52 +02:00
|
|
|
<button
|
|
|
|
class={
|
|
|
|
"btn btn-primary self-end btn-sm" +
|
|
|
|
(content() == "" ? " btn-disabled" : "")
|
|
|
|
}
|
|
|
|
onclick={() => {
|
|
|
|
if (content() == "") return;
|
2023-10-21 07:54:47 +02:00
|
|
|
createPost({
|
|
|
|
content: content(),
|
|
|
|
token: login_ctx?.token(),
|
|
|
|
} as NewPost);
|
2023-10-19 08:11:52 +02:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
Submit
|
|
|
|
</button>
|
2023-10-19 02:42:37 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function Posts() {
|
2023-10-19 08:11:52 +02:00
|
|
|
const [posts, setPosts] = createSignal([] as Post[]);
|
|
|
|
const [loading, setLoading] = createSignal(true);
|
2023-10-19 02:42:37 +02:00
|
|
|
|
|
|
|
getPosts().then((posts) => {
|
|
|
|
setPosts(posts as any);
|
2023-10-21 07:54:47 +02:00
|
|
|
setLoading(false);
|
2023-10-19 02:42:37 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
2023-10-19 08:11:52 +02:00
|
|
|
<div class="flex flex-col space-y-2 w-full md:w-96">
|
2023-10-21 07:54:47 +02:00
|
|
|
{loading() ? (
|
|
|
|
<span class="loading loading-spinner loading-lg self-center"></span>
|
|
|
|
) : (
|
|
|
|
<></>
|
|
|
|
)}
|
2023-10-19 02:42:37 +02:00
|
|
|
{posts().map((post) => {
|
2023-10-19 08:11:52 +02:00
|
|
|
if (post.content == "") return; // Filtering out empty posts, remove this later
|
|
|
|
return <PostSegment post={post}></PostSegment>;
|
2023-10-19 02:42:37 +02:00
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-19 08:11:52 +02:00
|
|
|
function PostSegment({ post }: { post: Post }) {
|
2023-10-19 02:42:37 +02:00
|
|
|
return (
|
2023-10-19 08:11:52 +02:00
|
|
|
<div class="card bg-base-200 shadow-lg compact text-base-content w-full">
|
2023-10-19 02:42:37 +02:00
|
|
|
<div class="card-body">
|
2023-10-21 07:54:47 +02:00
|
|
|
<p class="text-base-content break-words">{post.content}</p>
|
2023-10-19 02:42:37 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function Primary() {
|
|
|
|
return (
|
2023-10-19 08:11:52 +02:00
|
|
|
<Routes>
|
|
|
|
<Route path="/" element={<Posts />} />
|
|
|
|
<Route path="/new" element={<NewPostInputArea />} />
|
|
|
|
</Routes>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-21 07:54:47 +02:00
|
|
|
// This is a modal
|
2023-10-19 08:11:52 +02:00
|
|
|
function Login() {
|
2023-10-21 07:54:47 +02:00
|
|
|
const modal_ctx = useContext(ModalContext);
|
2023-10-19 08:11:52 +02:00
|
|
|
return (
|
2023-10-21 07:54:47 +02:00
|
|
|
<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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This function is responsible for sending the login request to the server
|
|
|
|
// and storing the token in localstorage
|
|
|
|
async function submitLogin(
|
|
|
|
username: string,
|
|
|
|
password: string
|
|
|
|
): Promise<string> {
|
|
|
|
const response = await fetch("/api/login", {
|
|
|
|
method: "POST",
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
body: JSON.stringify({ username, password }),
|
|
|
|
});
|
|
|
|
|
|
|
|
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 "";
|
|
|
|
}
|
|
|
|
|
|
|
|
function LoginForm() {
|
|
|
|
const modal_ctx = useContext(ModalContext);
|
|
|
|
const [username, setUsername] = createSignal("");
|
|
|
|
const [password, setPassword] = createSignal("");
|
|
|
|
const [waiting, setWaiting] = createSignal(false);
|
|
|
|
const [error, setError] = createSignal(false);
|
|
|
|
|
|
|
|
async function loginFailed() {
|
|
|
|
setError(true);
|
|
|
|
setWaiting(false);
|
|
|
|
setTimeout(() => {
|
|
|
|
setError(false);
|
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<form class="form-control">
|
|
|
|
<label class="label">
|
|
|
|
<span class="label-text">Username</span>
|
|
|
|
</label>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
placeholder="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"
|
|
|
|
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);
|
|
|
|
submitLogin(username(), password()).then((token) => {
|
|
|
|
if (token != "") {
|
|
|
|
setWaiting(false);
|
|
|
|
setError(false);
|
|
|
|
modal_ctx?.setLoginModalOpen(false);
|
|
|
|
} else {
|
|
|
|
loginFailed();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{waiting() ? "Logging in..." : "Login"}
|
|
|
|
</button>
|
|
|
|
</form>
|
2023-10-19 02:42:37 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Root;
|