Initial commit: Outlook Lite code app
This commit is contained in:
296
src/App.tsx
Normal file
296
src/App.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { outlookService } from './services/OutlookService';
|
||||
import type { Email, Folder } from './types';
|
||||
|
||||
const FOLDERS: Folder[] = [
|
||||
{ id: 'inbox', name: 'Bandeja de entrada', icon: '📥', unreadCount: 4 },
|
||||
{ id: 'sent', name: 'Enviados', icon: '📤', unreadCount: 0 },
|
||||
{ id: 'drafts', name: 'Borradores', icon: '📝', unreadCount: 1 },
|
||||
{ id: 'trash', name: 'Papelera', icon: '🗑️', unreadCount: 0 },
|
||||
];
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
if (diffMins < 1) return 'Ahora';
|
||||
if (diffMins < 60) return `${diffMins}m`;
|
||||
const diffHrs = Math.floor(diffMins / 60);
|
||||
if (diffHrs < 24) return `${diffHrs}h`;
|
||||
return d.toLocaleDateString('es-ES', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.slice(0, 2)
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [activeFolder, setActiveFolder] = useState<string>('inbox');
|
||||
const [emails, setEmails] = useState<Email[]>([]);
|
||||
const [selectedEmail, setSelectedEmail] = useState<Email | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCompose, setShowCompose] = useState(false);
|
||||
const [composeTo, setComposeTo] = useState('');
|
||||
const [composeSubject, setComposeSubject] = useState('');
|
||||
const [composeBody, setComposeBody] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const loadEmails = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msgs = await outlookService.getMessages(activeFolder);
|
||||
setEmails(msgs);
|
||||
setSelectedEmail(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmails();
|
||||
}, [loadEmails]);
|
||||
|
||||
const handleSelectEmail = async (email: Email) => {
|
||||
setSelectedEmail(email);
|
||||
if (!email.isRead) {
|
||||
await outlookService.markAsRead(email.id);
|
||||
setEmails((prev) =>
|
||||
prev.map((e) => (e.id === email.id ? { ...e, isRead: true } : e))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!composeTo || !composeSubject) return;
|
||||
setSending(true);
|
||||
try {
|
||||
await outlookService.sendMessage({
|
||||
to: composeTo,
|
||||
subject: composeSubject,
|
||||
body: composeBody,
|
||||
});
|
||||
setShowCompose(false);
|
||||
setComposeTo('');
|
||||
setComposeSubject('');
|
||||
setComposeBody('');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEmails = emails.filter(
|
||||
(e) =>
|
||||
e.subject.toLowerCase().includes(search.toLowerCase()) ||
|
||||
e.fromName.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
const activeFolderData = FOLDERS.find((f) => f.id === activeFolder)!;
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{/* Sidebar */}
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-icon" title="Outlook Lite">
|
||||
📧
|
||||
</div>
|
||||
<div
|
||||
className="sidebar-icon"
|
||||
title="Buscar"
|
||||
onClick={() => setSearch('')}
|
||||
>
|
||||
🔍
|
||||
</div>
|
||||
<div
|
||||
className="sidebar-icon"
|
||||
title="Redactar"
|
||||
onClick={() => setShowCompose(true)}
|
||||
>
|
||||
✏️
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="main-area">
|
||||
{/* Toolbar */}
|
||||
<div className="toolbar">
|
||||
<button
|
||||
className="toolbar-btn primary"
|
||||
onClick={() => setShowCompose(true)}
|
||||
>
|
||||
Redactar
|
||||
</button>
|
||||
<button className="toolbar-btn" onClick={loadEmails}>
|
||||
🔄 Recargar
|
||||
</button>
|
||||
<div className="search-box">
|
||||
<span>🔍</span>
|
||||
<input
|
||||
placeholder="Buscar en correos..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="content">
|
||||
{/* Folder list */}
|
||||
<div className="folder-list">
|
||||
{FOLDERS.map((folder) => (
|
||||
<div
|
||||
key={folder.id}
|
||||
className={`folder-item ${activeFolder === folder.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveFolder(folder.id)}
|
||||
>
|
||||
<span className="folder-icon">{folder.icon}</span>
|
||||
<span>{folder.name}</span>
|
||||
{folder.unreadCount > 0 && (
|
||||
<span className="unread">{folder.unreadCount}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Email list */}
|
||||
<div className="email-list">
|
||||
{loading ? (
|
||||
<div className="loading">
|
||||
<div className="spinner" />
|
||||
<span>Cargando...</span>
|
||||
</div>
|
||||
) : filteredEmails.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon">📭</div>
|
||||
<div className="empty-state-text">No hay correos en {activeFolderData.name}</div>
|
||||
</div>
|
||||
) : (
|
||||
filteredEmails.map((email) => (
|
||||
<div
|
||||
key={email.id}
|
||||
className={`email-item ${!email.isRead ? 'unread' : ''} ${selectedEmail?.id === email.id ? 'selected' : ''}`}
|
||||
onClick={() => handleSelectEmail(email)}
|
||||
>
|
||||
<div className="email-from">
|
||||
{email.fromName}
|
||||
<span className="email-time">{formatTime(email.receivedAt)}</span>
|
||||
</div>
|
||||
<div className="email-subject">{email.subject}</div>
|
||||
<div className="email-preview">
|
||||
{email.body.substring(0, 60)}…
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email detail */}
|
||||
{selectedEmail ? (
|
||||
<div className="email-detail">
|
||||
<div className="email-detail-header">
|
||||
<div className="email-avatar">
|
||||
{getInitials(selectedEmail.fromName)}
|
||||
</div>
|
||||
<div className="email-meta">
|
||||
<div className="email-meta-from">{selectedEmail.fromName}</div>
|
||||
<div className="email-meta-subject">{selectedEmail.subject}</div>
|
||||
<div className="email-meta-time">
|
||||
{new Date(selectedEmail.receivedAt).toLocaleString('es-ES', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="email-body">{selectedEmail.body}</div>
|
||||
<div className="email-actions">
|
||||
<button
|
||||
className="toolbar-btn"
|
||||
onClick={() => {
|
||||
setComposeTo(selectedEmail.from);
|
||||
setComposeSubject('Re: ' + selectedEmail.subject);
|
||||
setShowCompose(true);
|
||||
}}
|
||||
>
|
||||
↩️ Responder
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-btn"
|
||||
onClick={() => {
|
||||
setComposeTo(selectedEmail.from);
|
||||
setComposeSubject(selectedEmail.subject);
|
||||
setShowCompose(true);
|
||||
}}
|
||||
>
|
||||
↪️ Reenviar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon">📧</div>
|
||||
<div className="empty-state-text">Selecciona un correo para leerlo</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compose modal */}
|
||||
{showCompose && (
|
||||
<div className="compose-overlay" onClick={() => setShowCompose(false)}>
|
||||
<div className="compose" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="compose-header">
|
||||
<span>Nuevo mensaje</span>
|
||||
<button className="compose-close" onClick={() => setShowCompose(false)}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="compose-field">
|
||||
<span className="compose-field-label">Para</span>
|
||||
<input
|
||||
placeholder="destinatario@ejemplo.com"
|
||||
value={composeTo}
|
||||
onChange={(e) => setComposeTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="compose-field">
|
||||
<span className="compose-field-label">Asunto</span>
|
||||
<input
|
||||
placeholder="Asunto del mensaje"
|
||||
value={composeSubject}
|
||||
onChange={(e) => setComposeSubject(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="compose-body">
|
||||
<textarea
|
||||
placeholder="Escribe tu mensaje aquí..."
|
||||
value={composeBody}
|
||||
onChange={(e) => setComposeBody(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="compose-footer">
|
||||
<button
|
||||
className="toolbar-btn primary"
|
||||
onClick={handleSend}
|
||||
disabled={sending || !composeTo || !composeSubject}
|
||||
>
|
||||
{sending ? 'Enviando...' : '📤 Enviar'}
|
||||
</button>
|
||||
<button className="toolbar-btn" onClick={() => setShowCompose(false)}>
|
||||
Descartar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
444
src/index.css
Normal file
444
src/index.css
Normal file
@@ -0,0 +1,444 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--bg-surface: #0f3460;
|
||||
--accent: #e94560;
|
||||
--accent-hover: #ff6b6b;
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #a0a0b0;
|
||||
--border: #2a2a4a;
|
||||
--selected: #1e3a5f;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 60px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.sidebar-icon:hover {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.sidebar-icon.active {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* Main content area */
|
||||
.main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: var(--selected);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.toolbar-btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.toolbar-btn.primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Content area with folder list + email list + email detail */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Folder list */
|
||||
.folder-list {
|
||||
width: 180px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.folder-item:hover {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.folder-item.active {
|
||||
background: var(--selected);
|
||||
color: var(--text-primary);
|
||||
border-right: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.folder-item .folder-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.folder-item .unread {
|
||||
margin-left: auto;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Email list */
|
||||
.email-list {
|
||||
width: 320px;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.email-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.email-item:hover {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.email-item.selected {
|
||||
background: var(--selected);
|
||||
}
|
||||
|
||||
.email-item.unread {
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.email-from {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.email-item.unread .email-from {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.email-subject {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.email-preview {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.email-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Email detail */
|
||||
.email-detail {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.email-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.email-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.email-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.email-meta-from {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.email-meta-subject {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.email-meta-time {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.email-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Compose modal */
|
||||
.compose-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.compose {
|
||||
width: 600px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.compose-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.compose-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.compose-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.compose-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.compose-field-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.compose-field input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.compose-body {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.compose-body textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 180px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.compose-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
149
src/services/OutlookService.ts
Normal file
149
src/services/OutlookService.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { Email, UserProfile } from '../types';
|
||||
|
||||
// NOTE: In a real code app, these would be injected via the Power Platform
|
||||
// client library (@microsoft/power-apps-code-solutions or similar).
|
||||
// The client exposes typed services like Office365UsersService and
|
||||
// OutlookService that wrap the connector calls.
|
||||
//
|
||||
// For this demo, we simulate the connector responses with mock data.
|
||||
// To wire up real connectors:
|
||||
// 1. Create connections in make.powerapps.com (e.g. "shared_office365", "shared_outlook")
|
||||
// 2. Run: pac code add-data-source -a <apiName> -c <connectionId>
|
||||
// 3. The generator creates typed Model and Service files automatically
|
||||
//
|
||||
// Real service usage would look like:
|
||||
//
|
||||
// import { OutlookService } from './services/OutlookService';
|
||||
// const outlook = new OutlookService();
|
||||
// const emails = await outlook.getMessages('inbox');
|
||||
|
||||
const MOCK_USER: UserProfile = {
|
||||
displayName: 'Lago',
|
||||
email: 'jose@lago.dev',
|
||||
};
|
||||
|
||||
const MOCK_EMAILS: Email[] = [
|
||||
{
|
||||
id: '1',
|
||||
from: 'jose@lago.dev',
|
||||
fromName: 'Lago',
|
||||
to: 'lago@example.com',
|
||||
subject: 'Sprint planning — Thursday 10:00',
|
||||
body: 'Hola,\n\nVamos a planificar el sprint del mes que viene el jueves a las 10:00. Necesito que reviseis los temas pendientes del backlog.\n\nUn saludo',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
|
||||
isRead: false,
|
||||
folder: 'inbox',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
from: 'maria@partner.com',
|
||||
fromName: 'María García',
|
||||
to: 'lago@example.com',
|
||||
subject: 'RE: Presupuesto proyecto digital 2026',
|
||||
body: 'Hola Lago,\n\nGracias por la información. He revisado el presupuesto y parece correcto. Adjunto la propuesta final.\n\n¿Podemos hablar mañana por teléfono?\n\nSaludos',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(),
|
||||
isRead: false,
|
||||
folder: 'inbox',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
from: 'devops@company.com',
|
||||
fromName: 'DevOps Team',
|
||||
to: 'lago@example.com',
|
||||
subject: '[Alert] Production deployment failed — pipeline #4821',
|
||||
body: 'Pipeline #4821 failed at stage: build\n\nError: npm ERR! code ETARGET\nnpm ERR! notarget No valid target for react@19.0.1\n\nView logs: https://devops.company.com/pipelines/4821',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(),
|
||||
isRead: true,
|
||||
folder: 'inbox',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
from: 'lago@example.com',
|
||||
fromName: 'Lago',
|
||||
to: 'equipo@company.com',
|
||||
subject: 'Resumen semanal — Power Platform',
|
||||
body: 'Equipo,\n\nAquí va el resumen de lo hecho esta semana:\n\n• Demo de code apps publicada en el blog\n• Connector de Outlook integrado\n• Migración del backlog a Dataverse completada\n\nPara la semana que viene:\n• Terminar la integración con Teams\n• Revisar los permisos de producción\n\nSaludos',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
|
||||
isRead: true,
|
||||
folder: 'sent',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
from: 'newsletter@techweekly.com',
|
||||
fromName: 'Tech Weekly',
|
||||
to: 'lago@example.com',
|
||||
subject: 'This Week in AI & Power Platform — Issue #47',
|
||||
body: 'Top stories this week:\n\n1. Microsoft announces GPT-5 integration for Copilot Studio\n2. Power Apps code apps reach 1,400+ connectors milestone\n3. Dataverse 2026 Wave 1 features now generally available\n\nRead more inside...',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||
isRead: true,
|
||||
folder: 'inbox',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
from: 'noreply@linkedin.com',
|
||||
fromName: 'LinkedIn',
|
||||
to: 'lago@example.com',
|
||||
subject: '5 people viewed your profile this week',
|
||||
body: 'Your profile got noticed!\n\n5 people in Austria viewed your profile in the past week, including recruiters and hiring managers.\n\nView who they are →',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 60 * 72).toISOString(),
|
||||
isRead: true,
|
||||
folder: 'inbox',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
from: 'lago@example.com',
|
||||
fromName: 'Lago',
|
||||
to: 'draft',
|
||||
subject: 'Borrador: Propuesta reunión trimestral',
|
||||
body: 'Borrador de la propuesta para la reunión trimestral con dirección...',
|
||||
receivedAt: new Date(Date.now() - 1000 * 60 * 60 * 6).toISOString(),
|
||||
isRead: true,
|
||||
folder: 'drafts',
|
||||
},
|
||||
];
|
||||
|
||||
export class OutlookService {
|
||||
private delay(ms: number) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
async getProfile(): Promise<UserProfile> {
|
||||
await this.delay(300);
|
||||
return MOCK_USER;
|
||||
}
|
||||
|
||||
async getMessages(folder: string = 'inbox'): Promise<Email[]> {
|
||||
await this.delay(600);
|
||||
return MOCK_EMAILS.filter((e) => e.folder === folder);
|
||||
}
|
||||
|
||||
async getMessage(id: string): Promise<Email | undefined> {
|
||||
await this.delay(200);
|
||||
return MOCK_EMAILS.find((e) => e.id === id);
|
||||
}
|
||||
|
||||
async sendMessage(email: Partial<Email>): Promise<Email> {
|
||||
await this.delay(800);
|
||||
const newEmail: Email = {
|
||||
id: Date.now().toString(),
|
||||
from: MOCK_USER.email,
|
||||
fromName: MOCK_USER.displayName,
|
||||
to: email.to || '',
|
||||
subject: email.subject || '(Sin asunto)',
|
||||
body: email.body || '',
|
||||
receivedAt: new Date().toISOString(),
|
||||
isRead: true,
|
||||
folder: 'sent',
|
||||
};
|
||||
MOCK_EMAILS.push(newEmail);
|
||||
return newEmail;
|
||||
}
|
||||
|
||||
async markAsRead(id: string): Promise<void> {
|
||||
await this.delay(100);
|
||||
const email = MOCK_EMAILS.find((e) => e.id === id);
|
||||
if (email) email.isRead = true;
|
||||
}
|
||||
}
|
||||
|
||||
export const outlookService = new OutlookService();
|
||||
24
src/types/index.ts
Normal file
24
src/types/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface Email {
|
||||
id: string;
|
||||
from: string;
|
||||
fromName: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
receivedAt: string;
|
||||
isRead: boolean;
|
||||
folder: 'inbox' | 'sent' | 'drafts' | 'trash';
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
displayName: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user