Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_BACKEND_URL =
VITE_WS_URL =
Binary file modified bun.lockb
Binary file not shown.
80 changes: 42 additions & 38 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions public/splash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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");
}
31 changes: 31 additions & 0 deletions src/assets/styles/ChatLog.css
Original file line number Diff line number Diff line change
@@ -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;
}
39 changes: 17 additions & 22 deletions src/components/ChatBar.tsx
Original file line number Diff line number Diff line change
@@ -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<MessageType[]> }) {
const [content, setContent] = createSignal<string>("");
const isEmpty = createMemo(() => content().trim() === "");
if (!event.currentTarget) return;

const addMessage: JSX.EventHandler<HTMLFormElement, SubmitEvent> = 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 (
<form onSubmit={addMessage}>
<div class="box-border flex items-center rounded-lg bg-black/10 p-3 dark:bg-white/5">
<form onSubmit={formHandler}>
<div class="box-border flex items-center rounded-lg bg-gray-300 p-3 dark:bg-white/5">
<input
name="content"
class="h-full flex-1 bg-transparent text-black outline-none dark:text-white"
type="text"
placeholder="Send a messsage in #general"
value={content()}
onInput={e => setContent(e.target.value)}
autocomplete="off"
/>

<button
type="submit"
disabled={isEmpty()}
// disabled={isEmpty()}
class="w-6 opacity-40 hover:opacity-50 disabled:cursor-not-allowed disabled:opacity-20 dark:invert"
>
<img src="/app/icons/paper-plane.svg" />
Expand Down
35 changes: 29 additions & 6 deletions src/components/ChatLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MessageType[]> }) {
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 (
<div class="flex h-full flex-col gap-4 overflow-y-scroll" ref={elementReference}>
<For each={messages()}>{properties => <Message {...properties} />}</For>
<div
id="message-container"
class="flex h-full flex-col overflow-y-scroll"
ref={elementReference}
>
<For each={store.messages.sort((a, b) => Number(a.timestamp) - Number(b.timestamp))}>
{(data, index) => <Message message={data} grouped={shouldBeGrouped(data, index())} />}
</For>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/ColourModeSwitch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function ColourModeSwitch({ getter, setter }: Properties<boolean>
return (
<button
onClick={() => 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"
>
<Show when={getter()} fallback={fallback}>
<img src="/app/icons/moon.svg" />
Expand Down
63 changes: 63 additions & 0 deletions src/components/LoginTextInput.tsx
Original file line number Diff line number Diff line change
@@ -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<T extends z.ZodType> = keyof z.typeToFlattenedError<
z.TypeOf<T>
>["fieldErrors"];

type Props<T extends z.ZodType> = ComponentProps<"input"> & {
error: Accessor<z.typeToFlattenedError<z.TypeOf<T>> | null>;
};
// this has gotta be some kind of warcrime

export default function LoginTextInput<T extends z.ZodType>({
error,
type,
name,
...props
}: Props<T>) {
const [isHidden, setIsHidden] = createSignal<boolean>(true);
const [errorMessage, setErrorMessage] = createSignal<string | undefined>(undefined);

createEffect(() => {
const msg = error()?.fieldErrors[name as KeyOfFieldErrors<T>]?.[0];
setErrorMessage(msg);
});

const getType = (): string => {
if (type === "password") {
return isHidden() ? "password" : "text";
}

return "text";
};

return (
<div>
<div class="relative w-full">
<input
autocomplete="off"
type={getType()}
class="ring-gradient group peer w-full rounded-lg bg-white/10 p-3 font-semibold text-white outline-none ring-purple-600 transition focus:ring-2"
name={name}
{...props}
/>

{type === "password" && (
<button onClick={() => setIsHidden(!isHidden())} type="button">
{isHidden() ? (
<AiOutlineEye size={30} class="absolute right-3 top-0 h-full text-white" />
) : (
<AiOutlineEyeInvisible size={30} class="absolute right-3 top-0 h-full text-white" />
)}
</button>
)}
</div>

{errorMessage() && <p class="text-sm font-semibold text-red-500">*{errorMessage()}</p>}
</div>
);
}
46 changes: 37 additions & 9 deletions src/components/Message.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div class="box-border flex min-h-28 flex-col gap-2.5 rounded-lg bg-black/10 p-4 text-black dark:bg-white/10 dark:text-white">
<div class="text-md flex items-center gap-2.5 font-bold">
<img src="/app/cee.png" class="w-12 rounded-full" />
<div
class="group box-border flex w-full origin-bottom transform flex-row items-start gap-2 rounded-lg bg-gray-300 p-4 dark:bg-white/5"
style={{
"border-radius": grouped ? "0" : undefined,
transform: grouped ? "none" : undefined,
}}
data-grouped={grouped}
>
<div class="author flex gap-2">
<div class="mb-auto aspect-square size-6 hover:cursor-pointer">
<img
class="size-full rounded-full"
src="https://avatars.githubusercontent.com/u/190493638"
/>
</div>

<span>{properties.author}</span>
<span class="pt-1 text-xs text-black/40 dark:text-white/30">Today at 07:99</span>
<div class="mb-auto flex h-6 items-center">
<span class="whitespace-nowrap pr-2 font-bold text-gray-700 dark:text-white/85">
{message.author.username}
</span>
</div>
</div>

<div>
<p class="break-words">{properties.content}</p>
<div class="flex w-full flex-col gap-2.5">
<div class="group flex flex-row gap-2">
<p class="flex-1 text-gray-900 dark:text-white">{message.content}</p>

<p
class={clsx(
"h-full w-max select-none italic text-black/20 opacity-0 group-hover:opacity-100 dark:text-white",
"hover:opacity-20",
)}
>
{message.timestamp.calendar()}
</p>
</div>
</div>
</div>
);
Expand Down
Loading