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

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>
);
}