refactor: migrate from Next.js to SolidJS and GraphQL

- Converted web application from Next.js to SolidJS with Vite
- Replaced React components with SolidJS components
- Implemented GraphQL client using URQL
- Added authentication, room, and chat components
- Updated project structure and configuration files
- Removed unnecessary Next.js and docs-related files
- Added Docker support for web and API applications
This commit is contained in:
Juan Sebastián Montoya 2025-03-04 01:08:52 -05:00
parent 8f3aa2fc26
commit 16731409df
81 changed files with 13585 additions and 1163 deletions

367
apps/web/src/App.css Normal file
View file

@ -0,0 +1,367 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.solid:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
:root {
--primary-color: #4f46e5;
--primary-hover: #4338ca;
--secondary-color: #6b7280;
--background-color: #f9fafb;
--card-background: #ffffff;
--text-color: #1f2937;
--border-color: #e5e7eb;
--error-color: #ef4444;
--success-color: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
color: var(--text-color);
background-color: var(--background-color);
line-height: 1.5;
}
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: var(--primary-color);
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-main {
flex: 1;
padding: 1rem;
}
/* Auth styles */
.auth-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--card-background);
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.auth-tabs {
display: flex;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.auth-tabs button {
flex: 1;
padding: 0.75rem;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: var(--secondary-color);
}
.auth-tabs button.active {
color: var(--primary-color);
border-bottom: 2px solid var(--primary-color);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
font-size: 1rem;
}
.form-group.checkbox {
display: flex;
align-items: center;
}
.form-group.checkbox label {
display: flex;
align-items: center;
margin-bottom: 0;
}
.form-group.checkbox input {
width: auto;
margin-right: 0.5rem;
}
button {
padding: 0.75rem 1.5rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: var(--primary-hover);
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.error {
color: var(--error-color);
margin-bottom: 1rem;
padding: 0.5rem;
background-color: rgba(239, 68, 68, 0.1);
border-radius: 0.25rem;
}
.logout-button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
/* Chat styles */
.chat-container {
display: flex;
height: calc(100vh - 120px);
background-color: var(--card-background);
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.sidebar {
width: 300px;
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
}
.room-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.room-list h2 {
margin-bottom: 1rem;
font-size: 1.25rem;
}
.room-item {
padding: 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
margin-bottom: 0.5rem;
transition: background-color 0.2s;
}
.room-item:hover {
background-color: rgba(79, 70, 229, 0.1);
}
.room-item.selected {
background-color: rgba(79, 70, 229, 0.2);
}
.room-name {
font-weight: 500;
}
.room-description {
font-size: 0.875rem;
color: var(--secondary-color);
}
.chat-content {
flex: 1;
display: flex;
flex-direction: column;
}
.select-room-message {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
color: var(--secondary-color);
font-size: 1.125rem;
}
.chat-room {
display: flex;
flex-direction: column;
height: 100%;
}
.chat-header {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.chat-header h2 {
margin-bottom: 0.25rem;
}
.chat-header p {
color: var(--secondary-color);
font-size: 0.875rem;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.message {
max-width: 70%;
padding: 0.75rem;
border-radius: 0.5rem;
background-color: #f3f4f6;
align-self: flex-start;
}
.message.own-message {
background-color: rgba(79, 70, 229, 0.1);
align-self: flex-end;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.25rem;
font-size: 0.75rem;
}
.username {
font-weight: 500;
}
.time {
color: var(--secondary-color);
}
.message-content {
word-break: break-word;
}
.message-form {
display: flex;
padding: 1rem;
border-top: 1px solid var(--border-color);
}
.message-form input {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem 0 0 0.25rem;
font-size: 1rem;
}
.message-form button {
border-radius: 0 0.25rem 0.25rem 0;
}
/* Create Room styles */
.create-room {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.create-room-button {
width: 100%;
}
.create-room-form {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.create-room-form h3 {
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.chat-container {
flex-direction: column;
height: auto;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.chat-messages {
height: 50vh;
}
}

118
apps/web/src/App.tsx Normal file
View file

@ -0,0 +1,118 @@
import { createSignal, Show } from 'solid-js';
import { Provider } from '@urql/solid';
import { client } from './lib/graphql-client';
import { LoginForm } from './components/login-form';
import { RegisterForm } from './components/register-form';
import { RoomList } from './components/room-list';
import { ChatRoom } from './components/chat-room';
import { CreateRoom } from './components/create-room';
import './App.css';
function App() {
const [isAuthenticated, setIsAuthenticated] = createSignal(false);
const [userId, setUserId] = createSignal('');
const [selectedRoomId, setSelectedRoomId] = createSignal<string | null>(null);
const [showRegister, setShowRegister] = createSignal(false);
// Check if user is already authenticated
const checkAuth = () => {
const token = localStorage.getItem('token');
const storedUserId = localStorage.getItem('userId');
if (token && storedUserId) {
setIsAuthenticated(true);
setUserId(storedUserId);
}
};
// Call checkAuth on component mount
checkAuth();
const handleLoginSuccess = (token: string, id: string) => {
setIsAuthenticated(true);
setUserId(id);
};
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('userId');
setIsAuthenticated(false);
setUserId('');
setSelectedRoomId(null);
};
const handleSelectRoom = (roomId: string) => {
setSelectedRoomId(roomId);
};
return (
<Provider value={client}>
<div class='app'>
<header class='app-header'>
<h1>Unreal Chat</h1>
{isAuthenticated() && (
<button class='logout-button' onClick={handleLogout}>
Logout
</button>
)}
</header>
<main class='app-main'>
<Show
when={isAuthenticated()}
fallback={
<div class='auth-container'>
<div class='auth-tabs'>
<button
class={!showRegister() ? 'active' : ''}
onClick={() => setShowRegister(false)}
>
Login
</button>
<button
class={showRegister() ? 'active' : ''}
onClick={() => setShowRegister(true)}
>
Register
</button>
</div>
<Show
when={showRegister()}
fallback={<LoginForm onLoginSuccess={handleLoginSuccess} />}
>
<RegisterForm onRegisterSuccess={handleLoginSuccess} />
</Show>
</div>
}
>
<div class='chat-container'>
<aside class='sidebar'>
<CreateRoom onRoomCreated={handleSelectRoom} />
<RoomList
onSelectRoom={handleSelectRoom}
selectedRoomId={selectedRoomId() || undefined}
/>
</aside>
<div class='chat-content'>
<Show
when={selectedRoomId()}
fallback={
<div class='select-room-message'>
Select a room to start chatting
</div>
}
>
<ChatRoom roomId={selectedRoomId()!} userId={userId()} />
</Show>
</div>
</div>
</Show>
</main>
</div>
</Provider>
);
}
export default App;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,176 @@
import { createSignal, createEffect, For, Show } from 'solid-js';
import { gql } from '@urql/core';
import { createQuery, createMutation, createSubscription } from '@urql/solid';
import { Message, Room } from '../types';
const ROOM_QUERY = gql`
query GetRoom($id: ID!) {
room(id: $id) {
id
name
description
isPrivate
owner {
id
username
}
members {
id
username
}
}
}
`;
const MESSAGES_QUERY = gql`
query GetMessages($roomId: ID!) {
messages(roomId: $roomId) {
id
content
createdAt
user {
id
username
}
}
}
`;
const SEND_MESSAGE_MUTATION = gql`
mutation SendMessage($content: String!, $roomId: ID!) {
sendMessage(content: $content, roomId: $roomId) {
id
content
createdAt
user {
id
username
}
}
}
`;
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
content
createdAt
user {
id
username
}
}
}
`;
interface ChatRoomProps {
roomId: string;
userId: string;
}
export function ChatRoom(props: ChatRoomProps) {
const [message, setMessage] = createSignal('');
const [messages, setMessages] = createSignal<Message[]>([]);
// Query room details
const [roomQuery] = createQuery({
query: ROOM_QUERY,
variables: { id: props.roomId },
});
// Query messages
const [messagesQuery] = createQuery({
query: MESSAGES_QUERY,
variables: { roomId: props.roomId },
});
// Send message mutation
const [, sendMessage] = createMutation(SEND_MESSAGE_MUTATION);
// Subscribe to new messages
const [messageSubscription] = createSubscription({
query: MESSAGE_SUBSCRIPTION,
variables: { roomId: props.roomId },
});
// Load initial messages
createEffect(() => {
const result = messagesQuery;
if (result.data?.messages) {
setMessages(result.data.messages);
}
});
// Handle new messages from subscription
createEffect(() => {
const result = messageSubscription;
if (result.data?.messageAdded) {
const newMessage = result.data.messageAdded;
setMessages((prev) => [...prev, newMessage]);
}
});
const handleSendMessage = async (e: Event) => {
e.preventDefault();
if (!message().trim()) return;
try {
await sendMessage({
content: message(),
roomId: props.roomId,
});
setMessage('');
} catch (error) {
console.error('Failed to send message:', error);
}
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
return (
<div class='chat-room'>
<Show when={roomQuery.data?.room} fallback={<div>Loading room...</div>}>
<div class='chat-header'>
<h2>{roomQuery.data?.room.name}</h2>
<p>{roomQuery.data?.room.description}</p>
</div>
</Show>
<div class='chat-messages'>
<Show
when={!roomQuery.fetching}
fallback={<div>Loading messages...</div>}
>
<For each={messages()}>
{(message) => (
<div
class={`message ${message.user.id === props.userId ? 'own-message' : ''}`}
>
<div class='message-header'>
<span class='username'>{message.user.username}</span>
<span class='time'>{formatTime(message.createdAt)}</span>
</div>
<div class='message-content'>{message.content}</div>
</div>
)}
</For>
</Show>
</div>
<form class='message-form' onSubmit={handleSendMessage}>
<input
type='text'
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder='Type a message...'
/>
<button type='submit'>Send</button>
</form>
</div>
);
}

View file

@ -0,0 +1,112 @@
import { createSignal } from 'solid-js';
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid';
const CREATE_ROOM_MUTATION = gql`
mutation CreateRoom(
$name: String!
$description: String
$isPrivate: Boolean
) {
createRoom(name: $name, description: $description, isPrivate: $isPrivate) {
id
name
description
isPrivate
}
}
`;
interface CreateRoomProps {
onRoomCreated: (roomId: string) => void;
}
export function CreateRoom(props: CreateRoomProps) {
const [name, setName] = createSignal('');
const [description, setDescription] = createSignal('');
const [isPrivate, setIsPrivate] = createSignal(false);
const [error, setError] = createSignal('');
const [isOpen, setIsOpen] = createSignal(false);
const [state, executeMutation] = createMutation(CREATE_ROOM_MUTATION);
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!name().trim()) {
setError('Room name is required');
return;
}
try {
const result = await executeMutation({
name: name(),
description: description(),
isPrivate: isPrivate(),
});
if (result.error) {
setError(result.error.message);
return;
}
if (result.data?.createRoom) {
setName('');
setDescription('');
setIsPrivate(false);
setIsOpen(false);
props.onRoomCreated(result.data.createRoom.id);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
};
return (
<div class='create-room'>
<button class='create-room-button' onClick={() => setIsOpen(!isOpen())}>
{isOpen() ? 'Cancel' : 'Create Room'}
</button>
{isOpen() && (
<div class='create-room-form'>
<h3>Create a New Room</h3>
{error() && <div class='error'>{error()}</div>}
<form onSubmit={handleSubmit}>
<div class='form-group'>
<label for='room-name'>Room Name</label>
<input
type='text'
id='room-name'
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
required
/>
</div>
<div class='form-group'>
<label for='room-description'>Description (optional)</label>
<textarea
id='room-description'
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
/>
</div>
<div class='form-group checkbox'>
<label>
<input
type='checkbox'
checked={isPrivate()}
onChange={(e) => setIsPrivate(e.currentTarget.checked)}
/>
Private Room
</label>
</div>
<button type='submit' disabled={state.fetching}>
{state.fetching ? 'Creating...' : 'Create Room'}
</button>
</form>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,89 @@
import { createSignal } from 'solid-js';
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid';
const LOGIN_MUTATION = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
id
username
email
}
}
}
`;
interface LoginFormProps {
onLoginSuccess: (token: string, userId: string) => void;
}
export function LoginForm(props: LoginFormProps) {
const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal('');
const [error, setError] = createSignal('');
const [loginState, login] = createMutation(LOGIN_MUTATION);
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!email() || !password()) {
setError('Please enter both email and password');
return;
}
try {
const result = await login({ email: email(), password: password() });
if (result.error) {
setError(result.error.message);
return;
}
if (result.data?.login) {
const { token, user } = result.data.login;
localStorage.setItem('token', token);
localStorage.setItem('userId', user.id);
props.onLoginSuccess(token, user.id);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
};
return (
<div class='login-form'>
<h2>Login</h2>
{error() && <div class='error'>{error()}</div>}
<form onSubmit={handleSubmit}>
<div class='form-group'>
<label for='email'>Email</label>
<input
type='email'
id='email'
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
required
/>
</div>
<div class='form-group'>
<label for='password'>Password</label>
<input
type='password'
id='password'
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
<div>
<button type='submit' disabled={loginState.fetching}>
{loginState.fetching ? 'Logging in...' : 'Login'}
</button>
</div>
</form>
</div>
);
}

View file

@ -0,0 +1,118 @@
import { createSignal } from 'solid-js';
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid';
const REGISTER_MUTATION = gql`
mutation Register($email: String!, $username: String!, $password: String!) {
register(email: $email, username: $username, password: $password) {
token
user {
id
username
email
}
}
}
`;
interface RegisterFormProps {
onRegisterSuccess: (token: string, userId: string) => void;
}
export function RegisterForm(props: RegisterFormProps) {
const [email, setEmail] = createSignal('');
const [username, setUsername] = createSignal('');
const [password, setPassword] = createSignal('');
const [confirmPassword, setConfirmPassword] = createSignal('');
const [error, setError] = createSignal('');
const [state, executeMutation] = createMutation(REGISTER_MUTATION);
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!email() || !username() || !password() || !confirmPassword()) {
setError('Please fill in all fields');
return;
}
if (password() !== confirmPassword()) {
setError('Passwords do not match');
return;
}
try {
const result = await executeMutation({
email: email(),
username: username(),
password: password(),
});
if (result.error) {
setError(result.error.message);
return;
}
if (result.data?.register) {
const { token, user } = result.data.register;
localStorage.setItem('token', token);
localStorage.setItem('userId', user.id);
props.onRegisterSuccess(token, user.id);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
};
return (
<div class='register-form'>
<h2>Register</h2>
{error() && <div class='error'>{error()}</div>}
<form onSubmit={handleSubmit}>
<div class='form-group'>
<label for='email'>Email</label>
<input
type='email'
id='email'
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
required
/>
</div>
<div class='form-group'>
<label for='username'>Username</label>
<input
type='text'
id='username'
value={username()}
onInput={(e) => setUsername(e.currentTarget.value)}
required
/>
</div>
<div class='form-group'>
<label for='password'>Password</label>
<input
type='password'
id='password'
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
<div class='form-group'>
<label for='confirm-password'>Confirm Password</label>
<input
type='password'
id='confirm-password'
value={confirmPassword()}
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
required
/>
</div>
<button type='submit' disabled={state.fetching}>
{state.fetching ? 'Registering...' : 'Register'}
</button>
</form>
</div>
);
}

View file

@ -0,0 +1,89 @@
import { createSignal, createEffect, For, Show } from 'solid-js';
import { gql } from '@urql/core';
import { createQuery, createSubscription } from '@urql/solid';
import { Room } from '../types';
const ROOMS_QUERY = gql`
query GetRooms {
rooms {
id
name
description
isPrivate
createdAt
}
}
`;
const ROOM_ADDED_SUBSCRIPTION = gql`
subscription OnRoomAdded {
roomAdded {
id
name
description
isPrivate
createdAt
}
}
`;
interface RoomListProps {
onSelectRoom: (roomId: string) => void;
selectedRoomId?: string;
}
export function RoomList(props: RoomListProps) {
const [rooms, setRooms] = createSignal<Room[]>([]);
// Query rooms
const [roomsQuery] = createQuery({
query: ROOMS_QUERY,
});
// Subscribe to new rooms
const [roomAddedSubscription] = createSubscription({
query: ROOM_ADDED_SUBSCRIPTION,
});
// Load initial rooms
createEffect(() => {
const result = roomsQuery;
if (result.data?.rooms) {
setRooms(result.data.rooms);
}
});
// Handle new rooms from subscription
createEffect(() => {
const result = roomAddedSubscription;
if (result.data?.roomAdded) {
const newRoom = result.data.roomAdded;
setRooms((prev) => {
// Check if room already exists
if (prev.some((room) => room.id === newRoom.id)) {
return prev;
}
return [...prev, newRoom];
});
}
});
return (
<div class='room-list'>
<h2>Chat Rooms</h2>
<Show when={!roomsQuery.fetching} fallback={<div>Loading rooms...</div>}>
<For each={rooms()}>
{(room) => (
<div
class={`room-item ${props.selectedRoomId === room.id ? 'selected' : ''}`}
onClick={() => props.onSelectRoom(room.id)}
>
<div class='room-name'>{room.name}</div>
<div class='room-description'>{room.description}</div>
</div>
)}
</For>
</Show>
</div>
);
}

68
apps/web/src/index.css Normal file
View file

@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

8
apps/web/src/index.tsx Normal file
View file

@ -0,0 +1,8 @@
/* @refresh reload */
import { render } from 'solid-js/web'
import './index.css'
import App from './App.tsx'
const root = document.getElementById('root')
render(() => <App />, root!)

View file

@ -0,0 +1,50 @@
import { createClient, fetchExchange, subscriptionExchange } from '@urql/core';
import { createClient as createWSClient } from 'graphql-ws';
// Get API URLs from environment variables
const API_URL =
import.meta.env.VITE_API_URL || 'https://chat-api.jusemon.com/graphql';
const WS_URL =
import.meta.env.VITE_WS_URL || 'wss://chat-api.jusemon.com/graphql';
console.log('Current API_URL', API_URL);
console.log('Current WS_URL', WS_URL);
// Create a WebSocket client for GraphQL subscriptions
const wsClient = createWSClient({
url: WS_URL,
});
// Create the URQL client
export const client = createClient({
url: API_URL,
exchanges: [
fetchExchange,
subscriptionExchange({
forwardSubscription: (operation) => ({
subscribe: (sink) => {
const dispose = wsClient.subscribe(
{
...operation,
query: operation.query || '',
},
sink as any
);
return {
unsubscribe: dispose,
};
},
}),
}),
],
// For development, we'll add a simple header-based authentication
fetchOptions: () => {
const token = localStorage.getItem('token');
const userId = localStorage.getItem('userId');
return {
headers: {
'user-id': userId || '',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
};
},
});

View file

@ -0,0 +1,33 @@
export interface User {
id: string;
username: string;
email: string;
createdAt: string;
updatedAt: string;
}
export interface Room {
id: string;
name: string;
description?: string;
isPrivate: boolean;
createdAt: string;
updatedAt: string;
owner: User;
members: User[];
messages: Message[];
}
export interface Message {
id: string;
content: string;
createdAt: string;
updatedAt: string;
user: User;
room: Room;
}
export interface AuthPayload {
token: string;
user: User;
}

1
apps/web/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />