feat: Implement Share Modal for document sharing functionality

- Added ShareModal component to manage user and link sharing for documents.
- Created AuthContext to handle user authentication state and token management.
- Updated useYjsDocument hook to support sharing via tokens.
- Enhanced Yjs document creation to include user information and authentication tokens.
- Introduced AuthCallback page to handle authentication redirects and token processing.
- Modified EditorPage and KanbanPage to include share functionality.
- Created LoginPage with Google and GitHub authentication options.
- Added styles for LoginPage.
- Defined types for authentication and sharing in respective TypeScript files.
This commit is contained in:
M1ngdaXie
2026-01-06 22:03:07 -08:00
parent 8ae7fd96e8
commit 0a5e6661f1
30 changed files with 1923 additions and 118 deletions

View File

@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
function AuthCallback() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { login } = useAuth();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleCallback = async () => {
const token = searchParams.get('token');
const redirect = searchParams.get('redirect') || '/';
if (!token) {
setError('No authentication token received');
setTimeout(() => navigate('/login'), 2000);
return;
}
try {
await login(token);
navigate(redirect);
} catch (err) {
console.error('Login error:', err);
setError('Authentication failed. Please try again.');
setTimeout(() => navigate('/login'), 2000);
}
};
handleCallback();
}, [searchParams, login, navigate]);
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}>
<div style={{
background: 'white',
borderRadius: '16px',
padding: '48px',
textAlign: 'center',
}}>
{error ? (
<>
<h2 style={{ color: '#e53e3e', marginBottom: '8px' }}>Error</h2>
<p style={{ color: '#718096' }}>{error}</p>
<p style={{ color: '#718096', fontSize: '14px', marginTop: '16px' }}>
Redirecting to login...
</p>
</>
) : (
<>
<h2 style={{ color: '#1a202c', marginBottom: '8px' }}>Logging you in...</h2>
<p style={{ color: '#718096' }}>Please wait while we complete the authentication.</p>
</>
)}
</div>
</div>
);
}
export default AuthCallback;

View File

@@ -1,12 +1,18 @@
import { useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import Editor from "../components/Editor/Editor.tsx";
import UserList from "../components/Presence/UserList.tsx";
import ShareModal from "../components/Share/ShareModal.tsx";
import Navbar from "../components/Navbar.tsx";
import { useYjsDocument } from "../hooks/useYjsDocument.ts";
const EditorPage = () => {
const { id } = useParams<{ id: string }>();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { providers, synced } = useYjsDocument(id!);
const shareToken = searchParams.get('share') || undefined;
const { providers, synced } = useYjsDocument(id!, shareToken);
const [showShareModal, setShowShareModal] = useState(false);
if (!providers) {
return <div className="loading">Connecting...</div>;
@@ -14,10 +20,18 @@ const EditorPage = () => {
return (
<div className="editor-page">
<Navbar />
<div className="page-header">
<button onClick={() => navigate("/")}> Back to Home</button>
<div className="sync-status">
{synced ? "✓ Synced" : "⟳ Syncing..."}
<div className="header-actions">
<div className="sync-status">
{synced ? "✓ Synced" : "⟳ Syncing..."}
</div>
{!shareToken && (
<button className="share-btn" onClick={() => setShowShareModal(true)}>
Share
</button>
)}
</div>
</div>
@@ -29,6 +43,10 @@ const EditorPage = () => {
<UserList awareness={providers.awareness} />
</div>
</div>
{showShareModal && (
<ShareModal documentId={id!} onClose={() => setShowShareModal(false)} />
)}
</div>
);
};

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import type { DocumentType } from "../api/document.ts";
import { documentsApi } from "../api/document.ts";
import Navbar from "../components/Navbar.tsx";
import PixelIcon from "../components/PixelIcon/PixelIcon.tsx";
import FloatingGem from "../components/PixelSprites/FloatingGem.tsx";
@@ -57,12 +58,14 @@ const Home = () => {
}
return (
<div className="home-page" style={{ position: 'relative' }}>
<FloatingGem position={{ top: '20px', right: '40px' }} delay={0} size={40} />
<FloatingGem position={{ top: '60px', left: '60px' }} delay={1.5} size={32} />
<FloatingGem position={{ bottom: '100px', right: '100px' }} delay={3} size={36} />
<>
<Navbar />
<div className="home-page" style={{ position: 'relative' }}>
<FloatingGem position={{ top: '20px', right: '40px' }} delay={0} size={40} />
<FloatingGem position={{ top: '60px', left: '60px' }} delay={1.5} size={32} />
<FloatingGem position={{ bottom: '100px', right: '100px' }} delay={3} size={36} />
<h1>My Documents</h1>
<h1>My Documents</h1>
<div className="create-buttons">
<button onClick={() => createDocument("editor")} disabled={creating}>
@@ -105,7 +108,8 @@ const Home = () => {
))
)}
</div>
</div>
</div>
</>
);
};

View File

@@ -1,12 +1,18 @@
import { useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import KanbanBoard from "../components/Kanban/KanbanBoard.tsx";
import UserList from "../components/Presence/UserList.tsx";
import ShareModal from "../components/Share/ShareModal.tsx";
import Navbar from "../components/Navbar.tsx";
import { useYjsDocument } from "../hooks/useYjsDocument.ts";
const KanbanPage = () => {
const { id } = useParams<{ id: string }>();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { providers, synced } = useYjsDocument(id!);
const shareToken = searchParams.get('share') || undefined;
const { providers, synced } = useYjsDocument(id!, shareToken);
const [showShareModal, setShowShareModal] = useState(false);
if (!providers) {
return <div className="loading">Connecting...</div>;
@@ -14,10 +20,18 @@ const KanbanPage = () => {
return (
<div className="kanban-page">
<Navbar />
<div className="page-header">
<button onClick={() => navigate("/")}> Back to Home</button>
<div className="sync-status">
{synced ? "✓ Synced" : "⟳ Syncing..."}
<div className="header-actions">
<div className="sync-status">
{synced ? "✓ Synced" : "⟳ Syncing..."}
</div>
{!shareToken && (
<button className="share-btn" onClick={() => setShowShareModal(true)}>
Share
</button>
)}
</div>
</div>
@@ -29,6 +43,10 @@ const KanbanPage = () => {
<UserList awareness={providers.awareness} />
</div>
</div>
{showShareModal && (
<ShareModal documentId={id!} onClose={() => setShowShareModal(false)} />
)}
</div>
);
};

View File

@@ -0,0 +1,83 @@
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-container {
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
padding: 48px 40px;
max-width: 440px;
width: 100%;
text-align: center;
}
.login-title {
font-size: 32px;
font-weight: 700;
color: #1a202c;
margin: 0 0 8px 0;
}
.login-subtitle {
font-size: 16px;
color: #718096;
margin: 0 0 32px 0;
}
.login-buttons {
display: flex;
flex-direction: column;
gap: 16px;
}
.login-button {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.button-icon {
width: 20px;
height: 20px;
}
.google-button {
background: #4285f4;
color: white;
}
.google-button:hover {
background: #357ae8;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.3);
}
.github-button {
background: #24292e;
color: white;
}
.github-button:hover {
background: #1a1f23;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(36, 41, 46, 0.3);
}
.login-button:active {
transform: translateY(0);
}

View File

@@ -0,0 +1,86 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import './LoginPage.css';
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080/api";
function LoginPage() {
const { user, loading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && user) {
navigate('/');
}
}, [user, loading, navigate]);
const handleGoogleLogin = () => {
window.location.href = `${API_BASE_URL}/auth/google`;
};
const handleGitHubLogin = () => {
window.location.href = `${API_BASE_URL}/auth/github`;
};
if (loading) {
return (
<div className="login-page">
<div className="login-container">
<p>Loading...</p>
</div>
</div>
);
}
return (
<div className="login-page">
<div className="login-container">
<h1 className="login-title">Realtime Collab</h1>
<p className="login-subtitle">Collaborate in real-time with your team</p>
<div className="login-buttons">
<button
className="login-button google-button"
onClick={handleGoogleLogin}
>
<svg className="button-icon" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</button>
<button
className="login-button github-button"
onClick={handleGitHubLogin}
>
<svg className="button-icon" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"
/>
</svg>
Sign in with GitHub
</button>
</div>
</div>
</div>
);
}
export default LoginPage;