feat: enhance mail functionality with new data structure, improved UI components, and additional mail entries

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-04-02 02:43:36 -03:00
parent fa525f6090
commit d3bac607aa
30 changed files with 2730 additions and 595 deletions

21
components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -1,14 +1,19 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title> <title>General Bots</title>
</head> <link href="/output.css" rel="stylesheet" />
</head>
<body>
<body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

622
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "ESBUILD_RUNNER=true vite", "dev": "npx tailwindcss -i ./src/styles/globals.css -o ./public/output.css;ESBUILD_RUNNER=true vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri" "tauri": "tauri"
@ -12,12 +12,16 @@
"dependencies": { "dependencies": {
"@babel/runtime": "7.26.0", "@babel/runtime": "7.26.0",
"@hookform/resolvers": "3.9.1", "@hookform/resolvers": "3.9.1",
"@radix-ui/react-slot": "1.1.2",
"@tailwindcss/vite": "4.0.17",
"@tauri-apps/api": "2.4.0", "@tauri-apps/api": "2.4.0",
"@tauri-apps/plugin-opener": "2", "@tauri-apps/plugin-opener": "2",
"@zitadel/react": "1.0.5", "@zitadel/react": "1.0.5",
"autoprefixer": "10.4.17", "autoprefixer": "10.4.17",
"botframework-directlinejs": "0.15.1", "botframework-directlinejs": "0.15.1",
"botframework-webchat": "4.15.7", "botframework-webchat": "4.15.7",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"lucide-react": "0.454.0", "lucide-react": "0.454.0",
"nativewind": "2.0.10", "nativewind": "2.0.10",
@ -25,9 +29,10 @@
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-hook-form": "7.53.2", "react-hook-form": "7.53.2",
"react-markdown": "^10.1.0", "react-markdown": "10.1.0",
"react-router-dom": "7.4.1", "react-router-dom": "7.4.1",
"tailwindcss": "3.4.1", "tailwind-merge": "3.0.2",
"tailwindcss-animate": "1.0.7",
"uuid": "11.0.3", "uuid": "11.0.3",
"zod": "3.21.4" "zod": "3.21.4"
}, },
@ -35,15 +40,16 @@
"@babel/core": "7.18.6", "@babel/core": "7.18.6",
"@tauri-apps/cli": "2", "@tauri-apps/cli": "2",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/node": "22.13.14",
"@types/react": "18.3.1", "@types/react": "18.3.1",
"@types/react-dom": "18.3.1", "@types/react-dom": "18.3.1",
"@types/react-test-renderer": "18.0.7", "@types/react-test-renderer": "18.0.7",
"@vitejs/plugin-react": "4.3.4", "@vitejs/plugin-react": "4.3.4",
"copy-webpack-plugin": "12.0.2", "copy-webpack-plugin": "12.0.2",
"esbuild-runner": "^2.2.2", "esbuild-runner": "2.2.2",
"jest": "29.2.1", "jest": "29.2.1",
"postcss": "8.4.23", "postcss": "8.4.23",
"postcss-load-config": "^6.0.1", "postcss-load-config": "6.0.1",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"tailwindcss": "3.1.8", "tailwindcss": "3.1.8",
"typescript": "5.6.2", "typescript": "5.6.2",

View file

@ -1,7 +1,6 @@
// postcss.config.js module.exports = {
export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
} },
} }

1581
public/output.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["unstable"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View file

@ -2,7 +2,6 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default" "opener:default"

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tauri::{Emitter, Manager, Window}; use tauri::{Emitter, Window};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct FileItem { pub struct FileItem {

View file

@ -1,36 +1,68 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod drive; pub mod drive;
pub mod sync; pub mod sync;
use sync::AppState;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Duration;
use sync::AppState;
use tauri::{Manager, WebviewUrl};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command] #[tauri::command]
fn greet(name: &str) -> String { fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name) format!("Hello, {}! You've been greeted from Rust!", name)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
fn main() { pub fn main() {
tauri::Builder::default() tauri::Builder::default()
.manage(AppState { .setup(|app| {
sync_processes: Mutex::new(Vec::new()), let window = app.get_window("main").unwrap();
sync_active: Mutex::new(false),
// Hide window initially
window.hide()?;
// Remove decorations initially
window.set_decorations(false)?;
// Create a handle to the main window for the delayed show operation
let window_handle = window.clone();
// Use a delayed show operation without JS
std::thread::spawn(move || {
// Give time for the UI to initialize (adjust duration as needed)
std::thread::sleep(Duration::from_millis(500));
// Execute on main thread since window operations are not thread-safe
let window_for_closure = window_handle.clone();
let _ = window_handle.run_on_main_thread(move || {
window_for_closure.show().expect("Failed to show window");
window_for_closure
.set_decorations(true)
.expect("Failed to set decorations");
});
});
Ok(())
}) })
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
// Remove the now-unnecessary JS-dependent commands
sync::save_config, sync::save_config,
drive::list_files, drive::list_files,
drive::upload_file, // ... other commands
drive::create_folder,
sync::start_sync,
sync::stop_sync,
sync::get_status
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("Failed to run app");
} }
// These commands are no longer needed as we're handling the window display
// directly in the setup function
// #[tauri::command]
// fn show_main_window(window: tauri::Window) {
// window.show().expect("Failed to show window");
// }
//
// #[tauri::command]
// fn set_decorations(window: tauri::Window, value: bool) {
// window.set_decorations(value).expect("Failed to set decorations");
// }

View file

@ -1,9 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{Manager, Window};
use std::sync::Mutex; use std::sync::Mutex;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::path::Path; use std::path::Path;
use std::fs::{File, OpenOptions, create_dir_all}; use std::fs::{OpenOptions, create_dir_all};
use std::io::Write; use std::io::Write;
use std::env; use std::env;

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "my-tauri-app", "productName": "General Bots",
"version": "0.1.0", "version": "6.0.0",
"identifier": "online.generalbots", "identifier": "online.generalbots",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
@ -11,11 +11,18 @@
}, },
"app": { "app": {
"withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "my-tauri-app", "label": "main",
"title": "General Bots",
"width": 800, "width": 800,
"height": 600 "height": 600,
"resizable": true,
"fullscreen": false,
"visible": false,
"decorations": false,
"skipTaskbar": true
} }
], ],
"security": { "security": {

View file

@ -1,165 +0,0 @@
.app {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.menu-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.main-screen, .status-screen {
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
}
.dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 100;
display: flex;
flex-direction: column;
gap: 10px;
}
.status-list {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
}
.status-item {
background-color: #f0f0f0;
padding: 15px;
border-radius: 8px;
}
/* Existing styles below */
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

View file

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -1,12 +1,13 @@
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import AuthenticationScreen from './authentication'; import AuthenticationScreen from './authentication';
import { Chat } from './chat'; import { Chat } from './chat';
import {MailPage} from './mail'; import { MailPage } from './mail';
import DashboardPage from './dashboard'; import DashboardPage from './dashboard';
import TaskPage from './tasks'; import TaskPage from './tasks';
import TemplatesPage from './templates'; import TemplatesPage from './templates';
import {DriveScreen} from './drive'; import { DriveScreen } from './drive';
import SyncPage from './sync/page'; import SyncPage from './sync/page';
import { Button } from './components/ui/button';
const examples = [ const examples = [
{ name: "Home", href: "authentication" }, { name: "Home", href: "authentication" },
@ -21,10 +22,33 @@ const examples = [
{ name: "Help", href: "help" }, { name: "Help", href: "help" },
]; ];
const ExamplesNav = () => {
const location = useLocation();
const navigate = useNavigate();
return (
<div className="examples-nav-container">
<div className="examples-nav-inner">
{examples.map((example) => (
<Button
key={example.href}
onClick={() => navigate(example.href)}
className={`example-button ${location.pathname.includes(example.href) ? 'active' : ''
}`}
>
{example.name}
</Button>
))}
</div>
</div>
);
};
export function RootLayout() { export function RootLayout() {
return ( return (
<div className="app-container"> <div className="app-container">
Oi <ExamplesNav />
<main className="app-main"> <main className="app-main">
<Routes> <Routes>
<Route path="authentication" element={ <Route path="authentication" element={
@ -71,4 +95,4 @@ export function RootLayout() {
); );
} }
export default RootLayout; export default RootLayout;

6
src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -17,27 +17,37 @@ export function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps)
const selectedAccountData = accounts.find((account) => account.email === selectedAccount); const selectedAccountData = accounts.find((account) => account.email === selectedAccount);
// Triangle icon
const TriangleIcon = () => (
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 22.525H0l12-21.05 12 21.05z" />
</svg>
);
return ( return (
<div className="relative"> <div className="relative">
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-2'}`} className={`flex items-center w-full justify-between rounded-md px-2 py-1.5 hover:bg-gray-100 ${isCollapsed ? 'justify-center' : ''}`}
> >
{selectedAccountData && ( <div className="flex items-center gap-2">
<> <div className="bg-black rounded-md text-white p-1">
<div className="w-6 h-6 flex items-center justify-center"> <TriangleIcon />
{selectedAccountData.icon}
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<span>{selectedAccountData.label}</span> <span className="text-sm font-medium">Alicia Koch</span>
)} )}
</> </div>
{!isCollapsed && (
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m6 9 6 6 6-6" />
</svg>
)} )}
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute z-10 mt-2 w-56 bg-white rounded-md shadow-lg"> <div className="absolute z-10 top-full left-0 mt-1 w-64 rounded-md border shadow-md bg-white">
<div className="p-2"> <div className="p-1">
{accounts.map((account) => ( {accounts.map((account) => (
<button <button
key={account.email} key={account.email}
@ -45,12 +55,12 @@ export function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps)
setSelectedAccount(account.email); setSelectedAccount(account.email);
setIsOpen(false); setIsOpen(false);
}} }}
className="w-full text-left px-3 py-2 hover:bg-gray-100 flex items-center space-x-2" className="w-full text-left px-2 py-1.5 hover:bg-gray-100 rounded-md flex items-center gap-2"
> >
<div className="w-5 h-5 flex items-center justify-center"> <div className="w-6 h-6 flex items-center justify-center">
{account.icon} {account.icon}
</div> </div>
<span>{account.label}</span> <span className="text-sm">{account.label}</span>
</button> </button>
))} ))}
</div> </div>

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React from 'react';
import { format } from 'date-fns'; import { format } from 'date-fns';
interface Mail { interface Mail {
@ -8,6 +8,8 @@ interface Mail {
email: string; email: string;
date: string; date: string;
text: string; text: string;
read?: boolean;
labels?: string[];
} }
interface MailDisplayProps { interface MailDisplayProps {
@ -15,12 +17,10 @@ interface MailDisplayProps {
} }
export function MailDisplay({ mail }: MailDisplayProps) { export function MailDisplay({ mail }: MailDisplayProps) {
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
if (!mail) { if (!mail) {
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<p>No message selected</p> <p className="text-gray-500">No message selected</p>
</div> </div>
); );
} }
@ -29,7 +29,7 @@ export function MailDisplay({ mail }: MailDisplayProps) {
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-700">
{mail.name[0]} {mail.name[0]}
</div> </div>
<div> <div>
@ -39,7 +39,7 @@ export function MailDisplay({ mail }: MailDisplayProps) {
</div> </div>
</div> </div>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{format(new Date(mail.date), 'MMM d, yyyy h:mm a')} {format(new Date(mail.date), 'MMM d, yyyy, h:mm a')}
</span> </span>
</div> </div>
@ -49,16 +49,16 @@ export function MailDisplay({ mail }: MailDisplayProps) {
<div className="p-4 border-t"> <div className="p-4 border-t">
<textarea <textarea
placeholder={`Reply ${mail.name}...`} placeholder={`Reply to ${mail.name}...`}
className="w-full p-2 border rounded" className="w-full p-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500"
rows={4} rows={4}
/> />
<div className="flex justify-between items-center mt-2"> <div className="flex justify-between items-center mt-4">
<label className="flex items-center space-x-2"> <label className="flex items-center space-x-2 text-sm text-gray-700">
<input type="checkbox" /> <input type="checkbox" className="rounded text-blue-500" />
<span className="text-sm">Mute this thread</span> <span>Mute this thread</span>
</label> </label>
<button className="px-4 py-2 bg-blue-500 text-white rounded"> <button className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">
Send Send
</button> </button>
</div> </div>

View file

@ -14,36 +14,38 @@ interface Mail {
interface MailListProps { interface MailListProps {
items: Mail[]; items: Mail[];
selectedId?: string; selectedId?: string | null;
onSelect: (id: string) => void; onSelect: (id: string) => void;
} }
export function MailList({ items, selectedId, onSelect }: MailListProps) { export function MailList({ items, selectedId, onSelect }: MailListProps) {
return ( return (
<div className="space-y-2"> <div className="divide-y">
{items.map((item) => ( {items.map((item) => (
<div <div
key={item.id} key={item.id}
onClick={() => onSelect(item.id)} onClick={() => onSelect(item.id)}
className={`p-4 border rounded cursor-pointer ${selectedId === item.id ? 'bg-gray-100' : ''}`} className={`p-3 cursor-pointer ${
selectedId === item.id ? 'bg-gray-100' : ''
} hover:bg-gray-50`}
> >
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${item.read ? 'bg-transparent' : 'bg-blue-500'}`} /> <div className={`w-2 h-2 rounded-full ${item.read ? 'bg-transparent' : 'bg-blue-500'}`} />
<h3 className="font-medium">{item.name}</h3> <h3 className="font-medium text-sm">{item.name}</h3>
</div> </div>
<span className="text-sm text-gray-500"> <span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(item.date), { addSuffix: true })} {formatDistanceToNow(new Date(item.date), { addSuffix: true })}
</span> </span>
</div> </div>
<h4 className="font-medium mt-1">{item.subject}</h4> <h4 className="font-medium text-sm mt-1">{item.subject}</h4>
<p className="text-sm text-gray-500 mt-1 truncate"> <p className="text-xs text-gray-500 mt-1 truncate">
{item.text.substring(0, 100)}... {item.text.substring(0, 100)}...
</p> </p>
{item.labels.length > 0 && ( {item.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2"> <div className="flex flex-wrap gap-1 mt-2">
{item.labels.map((label) => ( {item.labels.map((label) => (
<span key={label} className="px-2 py-1 text-xs bg-gray-200 rounded"> <span key={label} className="px-2 py-0.5 text-xs bg-gray-200 rounded-full">
{label} {label}
</span> </span>
))} ))}

View file

@ -2,59 +2,230 @@ import React, { useState } from 'react';
import { AccountSwitcher } from './account-switcher'; import { AccountSwitcher } from './account-switcher';
import { MailList } from './mail-list'; import { MailList } from './mail-list';
import { MailDisplay } from './mail-display'; import { MailDisplay } from './mail-display';
import { Nav } from './nav';
import { mails, accounts } from '../data'; import { mails, accounts } from '../data';
export function Mail() { export function Mail() {
const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false);
const [selectedMailId, setSelectedMailId] = useState<string | null>(mails[0].id);
const [activeTab, setActiveTab] = useState('all'); const [activeTab, setActiveTab] = useState('all');
const [selectedMailId, setSelectedMailId] = useState<string | null>(null);
const toggleCollapse = () => setIsCollapsed(!isCollapsed);
const filteredMails = activeTab === 'all' const filteredMails = activeTab === 'all'
? mails ? mails
: mails.filter(mail => !mail.read); : mails.filter(mail => !mail.read);
// Icons
const ArchiveIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="20" height="5" x="2" y="3" rx="1" />
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
<path d="M10 12h4" />
</svg>
);
const TrashIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
);
const DeleteIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 6H4" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M3 6h2l1.5 14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2L21 6h2" />
</svg>
);
const ClockIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
);
const ArrowLeftIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>
);
const ArrowRightIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
);
const MoreIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>
);
const SearchIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
);
const navItems = [
{
title: "Inbox",
label: "128",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
</svg>
),
variant: "default" as const,
},
{
title: "Drafts",
label: "9",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
),
variant: "ghost" as const,
},
{
title: "Sent",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m22 2-7 20-4-9-9-4Z" />
<path d="M22 2 11 13" />
</svg>
),
variant: "ghost" as const,
},
{
title: "Junk",
label: "23",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="20" height="5" x="2" y="3" rx="1" />
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
<path d="M10 12h4" />
</svg>
),
variant: "ghost" as const,
},
{
title: "Trash",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
),
variant: "ghost" as const,
},
{
title: "Archive",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="20" height="5" x="2" y="3" rx="1" />
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
<path d="M10 12h4" />
</svg>
),
variant: "ghost" as const,
},
];
return ( return (
<div className="flex h-screen"> <div className="flex h-screen bg-white">
<div className={`${isCollapsed ? 'w-16' : 'w-64'} border-r flex flex-col`}> {/* Sidebar */}
<div className={`${isCollapsed ? 'w-16' : 'w-64'} border-r flex flex-col transition-all duration-200 bg-white`}>
<div className="p-4 border-b"> <div className="p-4 border-b">
<AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} /> <AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} />
</div> </div>
<div className="p-4"> <div className="flex-1 overflow-auto py-2">
<button onClick={toggleCollapse} className="w-full text-left"> <Nav links={navItems} isCollapsed={isCollapsed} />
{isCollapsed ? '»' : '« Collapse'} </div>
<div className="p-4 border-t">
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="w-full flex items-center justify-center text-sm text-gray-500 hover:text-gray-900"
>
{isCollapsed ? (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m9 18 6-6-6-6" />
</svg>
) : (
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6" />
</svg>
<span className="ml-2">Collapse</span>
</div>
)}
</button> </button>
</div> </div>
</div> </div>
<div className="flex-1 flex flex-col border-r"> {/* Mail list */}
<div className="p-4 border-b"> <div className="w-80 border-r flex flex-col">
<div className="p-4 border-b flex flex-col gap-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-xl font-bold">Inbox</h2> <h2 className="text-xl font-semibold">Inbox</h2>
<div className="flex space-x-2"> <div className="flex space-x-1">
<button <button className={`px-3 py-1.5 text-sm rounded-md ${activeTab === 'all' ? 'bg-gray-100 text-gray-900' : 'text-gray-500'}`} onClick={() => setActiveTab('all')}>
onClick={() => setActiveTab('all')}
className={`px-3 py-1 rounded ${activeTab === 'all' ? 'bg-blue-500 text-white' : ''}`}
>
All mail All mail
</button> </button>
<button <button className={`px-3 py-1.5 text-sm rounded-md ${activeTab === 'unread' ? 'bg-gray-100 text-gray-900' : 'text-gray-500'}`} onClick={() => setActiveTab('unread')}>
onClick={() => setActiveTab('unread')}
className={`px-3 py-1 rounded ${activeTab === 'unread' ? 'bg-blue-500 text-white' : ''}`}
>
Unread Unread
</button> </button>
</div> </div>
</div> </div>
<div className="mt-4">
<div className="flex relative">
<SearchIcon className="absolute left-3 top-2.5 h-4 w-4 text-gray-500" />
<input <input
type="text" type="text"
placeholder="Search" placeholder="Search"
className="w-full p-2 border rounded" className="w-full py-2 pl-9 pr-4 border rounded-md text-sm bg-transparent"
/> />
</div> </div>
</div> </div>
<div className="flex gap-1 border-b p-2">
<button className="p-1 rounded-md hover:bg-gray-100">
<ArchiveIcon />
</button>
<button className="p-1 rounded-md hover:bg-gray-100">
<TrashIcon />
</button>
<button className="p-1 rounded-md hover:bg-gray-100">
<DeleteIcon />
</button>
<span className="flex-1"></span>
<button className="p-1 rounded-md hover:bg-gray-100">
<ClockIcon />
</button>
<button className="p-1 rounded-md hover:bg-gray-100">
<ArrowLeftIcon />
</button>
<button className="p-1 rounded-md hover:bg-gray-100">
<ArrowRightIcon />
</button>
<button className="p-1 rounded-md hover:bg-gray-100">
<MoreIcon />
</button>
</div>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<MailList <MailList
items={filteredMails} items={filteredMails}
@ -64,6 +235,7 @@ export function Mail() {
</div> </div>
</div> </div>
{/* Mail content */}
<div className="flex-1"> <div className="flex-1">
<MailDisplay mail={mails.find(mail => mail.id === selectedMailId) || null} /> <MailDisplay mail={mails.find(mail => mail.id === selectedMailId) || null} />
</div> </div>

View file

@ -12,18 +12,20 @@ interface NavProps {
export function Nav({ links, isCollapsed }: NavProps) { export function Nav({ links, isCollapsed }: NavProps) {
return ( return (
<div className={`flex ${isCollapsed ? 'flex-col items-center' : 'flex-col'}`}> <div className="space-y-1 px-2">
{links.map((link, index) => ( {links.map((link, index) => (
<button <button
key={index} key={index}
className={`flex items-center p-2 rounded ${link.variant === 'default' ? 'bg-gray-100' : ''} ${isCollapsed ? 'justify-center' : 'justify-between'}`} className={`w-full flex items-center py-2 px-2 rounded-md text-sm ${
link.variant === 'default' ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-100'
} ${isCollapsed ? 'justify-center' : 'justify-between'}`}
> >
<div className="flex items-center"> <div className="flex items-center gap-3">
<span className="w-5 h-5 flex items-center justify-center"> <span className="w-5 h-5 flex items-center justify-center">
{link.icon} {link.icon}
</span> </span>
{!isCollapsed && ( {!isCollapsed && (
<span className="ml-2">{link.title}</span> <span>{link.title}</span>
)} )}
</div> </div>
{!isCollapsed && link.label && ( {!isCollapsed && link.label && (

View file

@ -1,3 +1,5 @@
import React from 'react';
export const mails = [ export const mails = [
{ {
id: "6c84fb90-12c4-11e1-840d-7b25c5ee775a", id: "6c84fb90-12c4-11e1-840d-7b25c5ee775a",
@ -19,7 +21,16 @@ export const mails = [
read: true, read: true,
labels: ["work", "important"], labels: ["work", "important"],
}, },
// More mails... {
id: "3e7c3f6d-bdf5-46ae-8d90-171300f27ae2",
name: "Bob Johnson",
email: "bob@example.com",
subject: "Weekend Plans",
text: "Any plans for the weekend? I was thinking of going hiking in the nearby mountains. It's supposed to be great weather and the fall colors should be at their peak.\n\nLet me know if you'd like to join. I'm planning to start early Saturday morning.\n\nCheers, Bob",
date: "2023-10-21T18:45:00",
read: false,
labels: ["personal"],
}
]; ];
export const accounts = [ export const accounts = [

View file

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { mails } from './data';
export function useMail() { export function useMail() {
const [mail, setMail] = useState({ const [mail, setMail] = useState({

View file

@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import RootLayout from "."; import RootLayout from ".";
import './styles/globals.css' // or your CSS file path
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>

3
src/styles/globals.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

201
styles.js
View file

@ -1,201 +0,0 @@
import { StyleSheet } from 'react-native';
export const colors = {
primary: '#8A4FFF',
secondary: '#FFD700',
background: '#2A1B3D',
white: '#FFFFFF',
purple: {
dark: '#1A0B2E',
medium: '#431E6E',
light: '#8A4FFF'
},
gold: {
light: '#FFD700',
medium: '#DAA520',
dark: '#B8860B'
}
};
const glassEffect = {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
};
export const globalStyles = StyleSheet.create({
container: {
flex: 1,
// Other global container styles
},
backgroundArcImage: {
position: 'absolute',
width: '355px',
height: '380px',
// Other global background image styles
},
backgroundImage: {
position: 'absolute',
width: '100%',
height: '100%',
// Other global background image styles
},
safeArea: {
flex: 1,
// Other global safe area styles
},
topNavContainer: {
width: '100%',
backgroundColor: 'transparent',
flexDirection: 'row',
alignItems: 'center',
},
innerNavContainer: {
width: '90%',
paddingTop: 20,
marginTop: -30,
borderRadius: 20,
height: 85,
marginLeft: '5%',
marginRight: '5%',
paddingRight: '20px',
paddingLeft: '20px',
backgroundColor: 'white',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
logoImage: {
left: 8,
top:4,
width: 80,
height: 20,
},
instagramIcon: {
right: 10,
width: 16,
height: 16,
},
link:{
color:'white'
},
headerView:{
display: 'inline'
},
logo: {
width: 48,
height: 48,
marginLeft: 8,
marginRight: 12,
borderRadius: 8,
backgroundColor: '#f0f0f0',
},
// Layout styles
container: {
flex: 1,
backgroundColor: 'white'
},
safeArea: {
flex: 1,
},
backgroundImage: {
top:0,
position: 'absolute',
width: '100%',
height: '100%',
resizeMode: 'cover',
},
backgroundImageForm: {
},
contentContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
card: {
width: '100%',
maxWidth: 400,
padding: 15,
borderRadius: 15,
...glassEffect,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
// Text styles
title: {
fontSize: 18,
flexWrap: 'nowrap',
fontWeight: '600',
marginBottom: 8,
textAlign: 'center',
color: colors.white,
letterSpacing: 0.5,
},
subtitle: {
fontSize: 14,
marginBottom: 25,
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.8)',
letterSpacing: 0.3,
},
// Input styles
input: {
width: '90%',
height: 38,
textAlign: 'center',
borderRadius: 8,
paddingHorizontal: 15,
marginBottom: 5,
marginRight: '5%',
marginLeft: '5%',
fontSize: 16,
backgroundColor: 'white',
color: colors.gray,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.15)',
},
button: {
marginTop: 20,
paddingVertical: 12,
marginLeft:16,
marginRight: 16,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#A855F7', // fallback color
// Add gradient-like effect using background image
backgroundImage: 'linear-gradient(to right, #7859a9, #deb99b)',
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
},
disabledButton: {
opacity: 0.7,
},
// Particle effects container
particleContainer: {
position: 'absolute',
width: '100%',
height: '100%',
pointerEvents: 'none',
},
})

59
tailwind.config.cjs Normal file
View file

@ -0,0 +1,59 @@
module.exports = {
darkMode: ["class"],
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
},
plugins: [require("tailwindcss-animate")],
}

View file

@ -1,12 +0,0 @@
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
// Add all files that use Tailwind classes
"./src/**/*.css"
],
theme: {
extend: {},
},
plugins: [],
}

View file

@ -1,5 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": [ "lib": [
@ -9,9 +13,6 @@
], ],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"types": [
"@tauri-apps/api/tauri"
],
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,

View file

@ -1,17 +1,26 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "path";
import tailwindcss from "@tailwindcss/vite"
// @ts-expect-error process is a nodejs global // @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(async () => ({ export default defineConfig(async () => ({
plugins: [react()],
plugins: [react(),tailwindcss()],
css: { css: {
postcss: { postcss: {
config: false // Disable auto-loading of postcss.config.js config: false // Disable auto-loading of postcss.config.js
} as any } as any
}, },
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// //
// 1. prevent vite from obscuring rust errors // 1. prevent vite from obscuring rust errors