diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..37be7b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_BACKEND_URL = +VITE_WS_URL = \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 836383c..cfc31cd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e55357c..ad6c02a 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,45 @@ { - "name": "lamna", - "version": "0.1.0", - "description": "", - "type": "module", - "scripts": { - "start": "vite", - "dev": "vite", - "dev:app": "tauri dev", - "dev:application": "tauri dev", - "build": "vite build", - "serve": "vite preview", - "tauri": "tauri", - "fmt": "prettier --write .", - "format": "prettier --write ." - }, - "license": "MIT", - "dependencies": { - "@fontsource/noto-sans": "^5.1.0", + "name": "lamna", + "version": "0.1.0", + "description": "", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "dev:app": "tauri dev", + "dev:application": "tauri dev", + "build": "vite build", + "serve": "vite preview", + "tauri": "tauri", + "fmt": "prettier --write .", + "format": "prettier --write ." + }, + "license": "MIT", + "dependencies": { + "@fontsource/noto-sans": "^5.1.0", + "@solid-primitives/websocket": "^1.2.2", "@solidjs/router": "^0.15.1", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-shell": "^2", - "clsx": "^2.1.1", - "solid-js": "^1.7.8" - }, - "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.4.0", - "@tauri-apps/cli": "^2", - "@tauri-apps/tauricon": "^1.0.3", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "prettier": "^3.4.1", - "prettier-plugin-sort-json": "^4.0.0", - "prettier-plugin-tailwindcss": "^0.6.9", - "tailwindcss": "^3.4.16", - "typescript": "^5.2.2", - "vite": "^5.3.1", - "vite-plugin-solid": "^2.8.0", - "vite-tsconfig-paths": "^5.1.4" - } + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-shell": "^2", + "clsx": "^2.1.1", + "moment": "^2.30.1", + "solid-icons": "^1.1.0", + "solid-js": "^1.7.8", + "zod": "^3.24.2" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.4.0", + "@tauri-apps/cli": "^2", + "@tauri-apps/tauricon": "^1.0.3", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "prettier": "^3.4.1", + "prettier-plugin-sort-json": "^4.0.0", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.16", + "typescript": "^5.2.2", + "vite": "^5.3.1", + "vite-plugin-solid": "^2.8.0", + "vite-tsconfig-paths": "^5.1.4" + } } diff --git a/public/splash.svg b/public/splash.svg new file mode 100644 index 0000000..7d63f58 --- /dev/null +++ b/public/splash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f91b35e..d6e1a43 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,14 +1,9 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ -#[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) -} #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/assets/styles/ChatLog.css b/src/assets/styles/ChatLog.css new file mode 100644 index 0000000..de5025e --- /dev/null +++ b/src/assets/styles/ChatLog.css @@ -0,0 +1,31 @@ +/* @import "tailwindcss"; */ + + +div[data-grouped='false'] { + + margin-top: 0.5rem; + margin-bottom: 0.5rem; + + border-radius: 0.5rem 0.5rem 0.5rem 0.5rem !important; +} + +[data-grouped=true]:not([data-grouped=true] + [data-grouped=true]) { + margin-top: 0.5rem; + + border-radius: 0.5rem 0.5rem 0 0 !important; +} + +[data-grouped=true]:has(+ :not([data-grouped=true])), +/* Select the last item of every 'group' */ +[data-grouped]:last-child + +/* If the item is the last item in the container */ + { + margin-bottom: 0.5rem; + + border-radius: 0 0 0.5rem 0.5rem !important; +} + +[data-grouped=true]+[data-grouped=true] .author { + display: none; +} \ No newline at end of file diff --git a/src/components/ChatBar.tsx b/src/components/ChatBar.tsx index b1eaf06..1733323 100644 --- a/src/components/ChatBar.tsx +++ b/src/components/ChatBar.tsx @@ -1,41 +1,36 @@ -import { createMemo, createSignal } from "solid-js"; +import { APIClient } from "~/libs/client"; -import type { MessageType } from "~/types"; - -import type { JSX, Setter } from "solid-js"; +export default function ChatBar() { + const formHandler = async (event: SubmitEvent) => { + event.preventDefault(); -export default function ChatBar({ setMessages }: { setMessages: Setter }) { - const [content, setContent] = createSignal(""); - const isEmpty = createMemo(() => content().trim() === ""); + if (!event.currentTarget) return; - const addMessage: JSX.EventHandler = event => { - event.preventDefault(); + const formData = new FormData(event.currentTarget as HTMLFormElement); + const content = formData.get("content"); - const newMessage: MessageType = { - id: Math.floor(Math.random() * 100_000), - author: "Big Balls Jr. Sr.", - content: content(), - }; + if (!content) return; - setMessages(previousMessages => [...previousMessages, newMessage]); - setContent(""); + try { + await APIClient.createMessage(String(content), 1); + } finally { + (event.target as HTMLFormElement).reset(); + } }; - return ( - - + + setContent(e.target.value)} autocomplete="off" /> + diff --git a/src/components/ChatLog.tsx b/src/components/ChatLog.tsx index 0d9ccb5..a4cdbab 100644 --- a/src/components/ChatLog.tsx +++ b/src/components/ChatLog.tsx @@ -2,24 +2,47 @@ import { createEffect, For } from "solid-js"; import Message from "~/components/Message"; -import type { MessageType } from "~/types"; -import type { Accessor } from "solid-js"; +import type { Message as MessageType } from "~/types"; +import type { Moment } from "moment"; -export default function ChatLog({ messages }: { messages: Accessor }) { +import "~/assets/styles/ChatLog.css"; + +import { store } from "~/libs/store"; + +export default function ChatLog() { let elementReference!: HTMLDivElement; const scrollToEnd = (_: any) => elementReference.scrollTo({ top: elementReference.scrollHeight, + behavior: "smooth", }); createEffect(() => { - scrollToEnd(messages()); + scrollToEnd(store.messages); }); + const isConsecutive = (before: Moment, after: Moment) => after.diff(before) < 1 * 60 * 1000; + + const shouldBeGrouped = (message: MessageType, idx: number): boolean => { + const previousMessage = store.messages[idx - 1]; + if (!previousMessage) return false; + + return ( + isConsecutive(previousMessage.timestamp, message.timestamp) && + previousMessage.author.id == message.author.id + ); + }; + return ( - - {properties => } + + Number(a.timestamp) - Number(b.timestamp))}> + {(data, index) => } + ); } diff --git a/src/components/ColourModeSwitch.tsx b/src/components/ColourModeSwitch.tsx index c09beda..5d46505 100644 --- a/src/components/ColourModeSwitch.tsx +++ b/src/components/ColourModeSwitch.tsx @@ -10,7 +10,7 @@ export default function ColourModeSwitch({ getter, setter }: Properties return ( setter(a => !a)} - class="absolute rounded-full bg-black p-2 transition-all dark:bg-white" + class="rounded-full bg-black p-2 transition-all dark:bg-white" > diff --git a/src/components/LoginTextInput.tsx b/src/components/LoginTextInput.tsx new file mode 100644 index 0000000..99e8979 --- /dev/null +++ b/src/components/LoginTextInput.tsx @@ -0,0 +1,63 @@ +import { AiOutlineEye, AiOutlineEyeInvisible } from "solid-icons/ai"; +import { createEffect, createSignal } from "solid-js"; +import { z } from "zod"; + +import type { Accessor, ComponentProps } from "solid-js"; + +type KeyOfFieldErrors = keyof z.typeToFlattenedError< + z.TypeOf +>["fieldErrors"]; + +type Props = ComponentProps<"input"> & { + error: Accessor> | null>; +}; +// this has gotta be some kind of warcrime + +export default function LoginTextInput({ + error, + type, + name, + ...props +}: Props) { + const [isHidden, setIsHidden] = createSignal(true); + const [errorMessage, setErrorMessage] = createSignal(undefined); + + createEffect(() => { + const msg = error()?.fieldErrors[name as KeyOfFieldErrors]?.[0]; + setErrorMessage(msg); + }); + + const getType = (): string => { + if (type === "password") { + return isHidden() ? "password" : "text"; + } + + return "text"; + }; + + return ( + + + + + {type === "password" && ( + setIsHidden(!isHidden())} type="button"> + {isHidden() ? ( + + ) : ( + + )} + + )} + + + {errorMessage() && *{errorMessage()}} + + ); +} diff --git a/src/components/Message.tsx b/src/components/Message.tsx index c960cd2..cba80fb 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -1,17 +1,45 @@ -import type { MessageType } from "~/types"; +import clsx from "clsx"; -export default function Message(properties: MessageType) { +import { Message as MessageType } from "~/types"; + +export default function Message({ message, grouped }: { message: MessageType; grouped: boolean }) { return ( - - - + + + + + - {properties.author} - Today at 07:99 + + + {message.author.username} + + - - {properties.content} + + + {message.content} + + + {message.timestamp.calendar()} + + ); diff --git a/src/index.tsx b/src/index.tsx index b9e7d3f..2a3efa3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,13 +6,25 @@ import "~/index.css"; import "@fontsource/noto-sans"; import RootLayout from "~/layouts/Root"; +import GlobalProvider from "~/libs/GlobalProvider"; import Home from "~/pages/Home"; +import Login from "~/pages/Login"; +import Signup from "~/pages/Signup"; +import Test from "~/pages/Test"; +import NormalView from "./layouts/NormalView"; render( () => ( - - - + + + + + + + + + + ), document.getElementById("root") as HTMLElement, ); diff --git a/src/layouts/NormalView.tsx b/src/layouts/NormalView.tsx new file mode 100644 index 0000000..9a2eeb7 --- /dev/null +++ b/src/layouts/NormalView.tsx @@ -0,0 +1,38 @@ +import { createWS } from "@solid-primitives/websocket"; +import moment from "moment"; + +import { APIClient } from "~/libs/client"; +import { setStore } from "~/libs/store"; + +export default function NormalView(props: any) { + const websocket = createWS(import.meta.env.VITE_WS_URL); + + APIClient.channelHistory(1).then(({ data }) => { + setStore("messages", messages => [...messages, ...data]); + }); + + websocket.addEventListener("message", (event: MessageEvent) => { + const message = JSON.parse(event.data); + const newMessage = { + ...message, + timestamp: moment(message.timestamp, moment.ISO_8601), + }; + + setStore("messages", messages => [...messages, newMessage]); + }); + + websocket.addEventListener("close", (_: CloseEvent) => { + // TODO: we need to handle this better, but for now this is fine. + setTimeout(() => { + window.location.reload(); + }, 10000); + }); + + setStore("websocket", websocket); + return ( + <> + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + {props.children} + > + ); +} diff --git a/src/layouts/Root.tsx b/src/layouts/Root.tsx index 762cda8..cd1e1cb 100644 --- a/src/layouts/Root.tsx +++ b/src/layouts/Root.tsx @@ -1,22 +1,16 @@ import clsx from "clsx"; import { createSignal } from "solid-js"; -import ColourModeSwitch from "~/components/ColourModeSwitch"; - export default function RootLayout(props: any) { const [isDarkMode, setIsDarkMode] = createSignal(true); return ( - - - - {props.children} ); diff --git a/src/libs/GlobalProvider.tsx b/src/libs/GlobalProvider.tsx new file mode 100644 index 0000000..b559de7 --- /dev/null +++ b/src/libs/GlobalProvider.tsx @@ -0,0 +1,39 @@ +import { createWS } from "@solid-primitives/websocket"; +import moment from "moment"; +import { ParentProps } from "solid-js"; + +import { setStore } from "~/libs/store"; +import { APIClient } from "./client"; + +type Properties = ParentProps; + +export default function GlobalProvider(properties: Properties) { + return <>{properties.children}>; + const websocket = createWS(import.meta.env.VITE_WS_URL); + + APIClient.channelHistory(1).then(({ data }) => { + setStore("messages", messages => [...messages, ...data]); + }); + + websocket.addEventListener("message", (event: MessageEvent) => { + const message = JSON.parse(event.data); + const newMessage = { + ...message, + timestamp: moment(message.timestamp, moment.ISO_8601), + }; + + setStore("messages", messages => [...messages, newMessage]); + }); + + websocket.addEventListener("close", (_: CloseEvent) => { + // TODO: we need to handle this better, but for now this is fine. + setTimeout(() => { + window.location.reload(); + }, 10000); + }); + + setStore("websocket", websocket); + + // return {properties.children}; + return <>{properties.children}>; +} diff --git a/src/libs/client.ts b/src/libs/client.ts new file mode 100644 index 0000000..b945758 --- /dev/null +++ b/src/libs/client.ts @@ -0,0 +1,192 @@ +import moment from "moment"; + +import { Message, User } from "~/types"; +import { tempGetCookie, tempSetCookie } from "./store"; + +type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; +type RequestResponse = { data: T; status: number }; + +class Route { + private static BASE = `${import.meta.env.VITE_BACKEND_URL}/api/v1`; + + method: Method; + path: string; + query?: Record; + slug?: Record; + + constructor( + method: Method, + path: string, + options?: { query?: Record; slug?: Record }, + ) { + this.method = method; + this.path = path; + this.query = options?.query; + this.slug = options?.slug; + } + + private substitute(path: string, lookup: Record): string { + return path.replace(/{([^}]+)}/g, (_, key) => { + const slug = lookup[key]; + if (!slug) { + throw new Error(`Could not substitute ${key} in ${path}.`); + } + + return slug; + }); + } + + get url() { + const path = this.slug ? this.substitute(this.path, this.slug) : this.path; + + let url = `${Route.BASE}${path}`; + if (this.query) { + const params = new URLSearchParams(this.query); + url += `?${params.toString()}`; + } + + return url; + } +} + +class Client { + private token?: string; + private refresh_token?: string; + + private getHeaders(route: Route): Record { + if (route.path === "/refresh") { + if (!this.refresh_token) { + this.refresh_token = tempGetCookie("lamna-refresh"); + } + return { Authorisation: `Bearer ${this.refresh_token}` }; + } else { + if (!this.token) { + this.token = tempGetCookie("lamna-auth"); + } + return { Authorisation: `Bearer ${this.token}` }; + } + } + + private async request( + route: Route, + body?: Record, + options?: { + refresh: boolean; + }, + ): Promise> { + const authHeaders = this.getHeaders(route); + + const response = await fetch(route.url, { + body: body ? JSON.stringify(body) : null, + method: route.method, + headers: { + "Content-Type": "application/json", + "User-Agent": `LamnaClient (v0)`, + ...authHeaders, + }, + credentials: "include", + }); + + if (!response.ok) { + if (response.status === 401) { + if (options?.refresh ?? true) { + await this.refresh(); + return await this.request(route, body); + } + } + const error = await response.text(); + + throw new Error(error); + } + + return { + data: await response.json(), + status: response.status, + }; + } + + public async refresh() { + const route = new Route("POST", "/refresh"); + + type RefreshResponse = { auth_token: string; refresh_token: string }; + const { data } = await this.request(route); + + this.token = data.auth_token; + this.refresh_token = data.refresh_token; + + tempSetCookie("lamna-auth", this.token); + tempSetCookie("lamna-refresh", this.refresh_token); + } + + public async login(username: string, password: string) { + const route = new Route("POST", "/login"); + + type LoginResponse = { + user: User; + auth_token: string; + refresh_token: string; + }; + + const response = await this.request( + route, + { + username, + password, + }, + { refresh: false }, + ); + + return response; + } + + public async me() { + const route = new Route("GET", "/@me"); + + const response = await this.request(route); + return response; + } + + public async createMessage(content: string, channel_id: number) { + const route = new Route("POST", "/channels/{channel_id}/messages", { slug: { channel_id } }); + + const response = await this.request(route, { content }); + + return response; + } + + public async channelHistory(channel_id: number) { + const route = new Route("GET", "/channels/{channel_id}/messages", { slug: { channel_id } }); + + const response = await this.request(route); + + response.data = response.data.map(message => { + return { + ...message, + timestamp: moment(message.timestamp, moment.ISO_8601), + } satisfies Message; + }); + + return response; + } + + public async signup(username: string, email: string, password: string) { + const route = new Route("POST", "/signup"); + + type SignupResponse = { + user: User; + auth_token: string; + refresh_token: string; + }; + + const response = await this.request(route, { + username, + email, + password, + }); + + return response; + } +} + +// probably delegate creation to iother file +export const APIClient = new Client(); diff --git a/src/libs/context.ts b/src/libs/context.ts new file mode 100644 index 0000000..979fd56 --- /dev/null +++ b/src/libs/context.ts @@ -0,0 +1,10 @@ +import { Context, createContext } from "solid-js"; +import { Store } from "solid-js/store"; + +import type { StoreData } from "~/libs/store"; + +type GlobalContextData = { + store: Store; +}; + +export const GlobalContext: Context = createContext(undefined); diff --git a/src/libs/store.ts b/src/libs/store.ts new file mode 100644 index 0000000..2f77760 --- /dev/null +++ b/src/libs/store.ts @@ -0,0 +1,34 @@ +import { createStore, SetStoreFunction } from "solid-js/store"; + +import type { Message } from "~/types"; + +export type StoreData = { + websocket?: WebSocket; + messages: Message[]; + auth?: { auth: string; refresh: string }; + isAuthed: boolean; + user?: { username: string; id: string }; +}; + +export const [store, setStore]: [store: StoreData, setStore: SetStoreFunction] = + createStore({ + messages: [], + isAuthed: false, + }); + +export const tempSetCookie = (name: string, token: string) => { + return; + // As you can see, proof-of-concept: + document.cookie = `${name}=${token}; expires=Sat 01 December 2025 00:00:00 UTC; domain=100.88.207.41; path=/;`; +}; + +export const tempGetCookie = (name: string): string => { + const cookies: Record = document.cookie + .split("; ") + .reduce((acc: Record, cookie) => { + const [key, value] = cookie.split("="); + acc[key] = value; + return acc; + }, {}); + return cookies[name]; +}; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 70a9d1b..97b9e22 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,18 +1,23 @@ -import { createSignal } from "solid-js"; +import { useNavigate } from "@solidjs/router"; +import { createEffect } from "solid-js"; import ChatBar from "~/components/ChatBar"; import ChatLog from "~/components/ChatLog"; - -import type { MessageType } from "~/types"; +import { store } from "~/libs/store"; export default function Home() { - // TODO: Retrieve externally - const [messages, setMessages] = createSignal([]); + const navigate = useNavigate(); + + createEffect(() => { + if (!store.isAuthed) { + navigate("/login"); + } + }); return ( <> - - + + > ); } diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..d6e0890 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,116 @@ +import { A, useNavigate } from "@solidjs/router"; +import { createSignal } from "solid-js"; +import { z } from "zod"; + +import LoginTextInput from "~/components/LoginTextInput"; +import { APIClient } from "~/libs/client"; +import { setStore, tempSetCookie } from "~/libs/store"; + +const LoginData = z.object({ + username: z.string().nonempty("Invalid username").trim(), + password: z.string().nonempty("Invalid password").trim(), +}); + +export default function Login() { + let passwordInput!: HTMLInputElement; + const navigate = useNavigate(); + + const [formErrors, setFormError] = createSignal + > | null>(null); + + const onSubmit = async (event: SubmitEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget as HTMLFormElement); + + const parsed = LoginData.safeParse({ + username: data.get("username"), + password: data.get("password"), + }); + + if (!parsed.success) { + return setFormError(parsed.error.flatten()); + } + + const { status } = await APIClient.login(parsed.data.username, parsed.data.password); + + if (status === 401) { + passwordInput.value = ""; + return setFormError({ formErrors: ["Wrong username or password."], fieldErrors: {} }); + } else if (status === 200) { + navigate("/"); + } + }; + + return ( + <> + + + + + + + + Welcome to{" "} + + Lamna + + + + + + + + + + + + + + Remember me? + + + + Log in + + + + + + {formErrors()?.["formErrors"][0] && ( + {formErrors()?.["formErrors"][0]} + )} + + + + I forgot my password. + + + Don't have an account? + + Sign up. + + + + + + > + ); +} diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx new file mode 100644 index 0000000..06369b4 --- /dev/null +++ b/src/pages/Signup.tsx @@ -0,0 +1,116 @@ +import { A, useNavigate } from "@solidjs/router"; +import { createSignal } from "solid-js"; +import { z } from "zod"; + +import LoginTextInput from "~/components/LoginTextInput"; +import { APIClient } from "~/libs/client"; +import { setStore, tempSetCookie } from "~/libs/store"; + +const SignInData = z.object({ + email: z.string().nonempty().email("Invalid email"), + username: z.string().nonempty("Invalid username").trim(), + password: z.string().nonempty("Invalid password").min(8).trim(), +}); + +export default function Signup() { + const navigate = useNavigate(); + + const [formErrors, setFormError] = createSignal + > | null>(null); + + const onSubmit = async (event: SubmitEvent) => { + event.preventDefault(); + + const data = new FormData(event.currentTarget as HTMLFormElement); + + const parsed = SignInData.safeParse({ + email: data.get("email"), + username: data.get("username"), + password: data.get("password"), + }); + + if (!parsed.success) { + return setFormError(parsed.error.flatten()); + } + + const parsedData = parsed.data!; + + const { data: resp, status } = await APIClient.signup( + parsedData.username, + parsedData.email, + parsedData.password, + ); + + if (status === 409) { + setFormError({ formErrors: ["User already exists"], fieldErrors: {} }); + } else if (status === 200) { + setStore("auth", { auth: resp.auth_token, refresh: resp.refresh_token }); + + setStore("isAuthed", true); + navigate("/"); + + setStore("user", { username: resp.user.username, id: resp.user.id }); + tempSetCookie("lamna-auth", resp.auth_token); + } + }; + + return ( + <> + + + + + + + Create an account + + + + + + + + + + + Sign up + + + + + + {/* TODO: Fix styling for error text */} + {formErrors()?.["formErrors"]?.[0] && ( + {formErrors()?.["formErrors"]?.[0]} + )} + + + + Already have an acount? + + Log in. + + + + + + > + ); +} diff --git a/src/pages/Test.tsx b/src/pages/Test.tsx new file mode 100644 index 0000000..ba8346b --- /dev/null +++ b/src/pages/Test.tsx @@ -0,0 +1,27 @@ +import { createSignal } from "solid-js"; + +import { APIClient } from "~/libs/client"; + +export default function Test() { + const client = APIClient; + const onClick = async () => { + await client.refresh(); + }; + + const getInfo = async () => { + const { data: info } = await client.me(); + setInfo({ id: info.id, username: info.username }); + }; + + const [info, setInfo] = createSignal<{ id?: string; username?: string }>({}); + return ( + + + onClick()}>Refresh token + getInfo()}>Get Info + ID: {info().id} + Username: {info().username} + + + ); +} diff --git a/src/types.d.ts b/src/types.d.ts index 9871e84..15a3991 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,3 +1,5 @@ +import { Moment } from "moment"; + import type { Accessor, Setter } from "solid-js"; export interface Properties { @@ -5,8 +7,22 @@ export interface Properties { setter: Setter; } -export interface MessageType { - id: number; - author: string; +export interface User { + id: string; + username: string; + bot: boolean; + staff: boolean; +} + +export interface Message { + id: string; content: string; + timestamp: moment; + author: User; + channelId: string; +} + +export interface CachedUser { + id: string; + username: string; }
*{errorMessage()}
{properties.content}
{message.content}
+ {message.timestamp.calendar()} +
{formErrors()?.["formErrors"][0]}
+ Don't have an account? + + Sign up. + +
{formErrors()?.["formErrors"]?.[0]}
+ Already have an acount? + + Log in. + +
ID: {info().id}
Username: {info().username}