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>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
<title>General Bots</title>
<link href="/output.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</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",
"type": "module",
"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",
"preview": "vite preview",
"tauri": "tauri"
@ -12,12 +12,16 @@
"dependencies": {
"@babel/runtime": "7.26.0",
"@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/plugin-opener": "2",
"@zitadel/react": "1.0.5",
"autoprefixer": "10.4.17",
"botframework-directlinejs": "0.15.1",
"botframework-webchat": "4.15.7",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "2.30.0",
"lucide-react": "0.454.0",
"nativewind": "2.0.10",
@ -25,9 +29,10 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "7.53.2",
"react-markdown": "^10.1.0",
"react-markdown": "10.1.0",
"react-router-dom": "7.4.1",
"tailwindcss": "3.4.1",
"tailwind-merge": "3.0.2",
"tailwindcss-animate": "1.0.7",
"uuid": "11.0.3",
"zod": "3.21.4"
},
@ -35,15 +40,16 @@
"@babel/core": "7.18.6",
"@tauri-apps/cli": "2",
"@types/jest": "29.5.12",
"@types/node": "22.13.14",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",
"@types/react-test-renderer": "18.0.7",
"@vitejs/plugin-react": "4.3.4",
"copy-webpack-plugin": "12.0.2",
"esbuild-runner": "^2.2.2",
"esbuild-runner": "2.2.2",
"jest": "29.2.1",
"postcss": "8.4.23",
"postcss-load-config": "^6.0.1",
"postcss-load-config": "6.0.1",
"react-test-renderer": "18.2.0",
"tailwindcss": "3.1.8",
"typescript": "5.6.2",

View file

@ -1,7 +1,6 @@
// postcss.config.js
export default {
module.exports = {
plugins: {
tailwindcss: {},
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 = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["unstable"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View file

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

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use tauri::{Emitter, Manager, Window};
use tauri::{Emitter, Window};
#[derive(Debug, Serialize, Deserialize)]
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")]
pub mod drive;
pub mod sync;
use sync::AppState;
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]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
fn main() {
pub fn main() {
tauri::Builder::default()
.manage(AppState {
sync_processes: Mutex::new(Vec::new()),
sync_active: Mutex::new(false),
.setup(|app| {
let window = app.get_window("main").unwrap();
// 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![
// Remove the now-unnecessary JS-dependent commands
sync::save_config,
drive::list_files,
drive::upload_file,
drive::create_folder,
sync::start_sync,
sync::stop_sync,
sync::get_status
// ... other commands
])
.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 tauri::{Manager, Window};
use std::sync::Mutex;
use std::process::{Command, Stdio};
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::env;

View file

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

@ -7,6 +7,7 @@ import TaskPage from './tasks';
import TemplatesPage from './templates';
import { DriveScreen } from './drive';
import SyncPage from './sync/page';
import { Button } from './components/ui/button';
const examples = [
{ name: "Home", href: "authentication" },
@ -21,10 +22,33 @@ const examples = [
{ 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() {
return (
<div className="app-container">
Oi
<ExamplesNav />
<main className="app-main">
<Routes>
<Route path="authentication" element={

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);
// 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 (
<div className="relative">
<button
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="w-6 h-6 flex items-center justify-center">
{selectedAccountData.icon}
<div className="flex items-center gap-2">
<div className="bg-black rounded-md text-white p-1">
<TriangleIcon />
</div>
{!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>
{isOpen && (
<div className="absolute z-10 mt-2 w-56 bg-white rounded-md shadow-lg">
<div className="p-2">
<div className="absolute z-10 top-full left-0 mt-1 w-64 rounded-md border shadow-md bg-white">
<div className="p-1">
{accounts.map((account) => (
<button
key={account.email}
@ -45,12 +55,12 @@ export function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps)
setSelectedAccount(account.email);
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}
</div>
<span>{account.label}</span>
<span className="text-sm">{account.label}</span>
</button>
))}
</div>

View file

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

View file

@ -14,36 +14,38 @@ interface Mail {
interface MailListProps {
items: Mail[];
selectedId?: string;
selectedId?: string | null;
onSelect: (id: string) => void;
}
export function MailList({ items, selectedId, onSelect }: MailListProps) {
return (
<div className="space-y-2">
<div className="divide-y">
{items.map((item) => (
<div
key={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 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'}`} />
<h3 className="font-medium">{item.name}</h3>
<h3 className="font-medium text-sm">{item.name}</h3>
</div>
<span className="text-sm text-gray-500">
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(item.date), { addSuffix: true })}
</span>
</div>
<h4 className="font-medium mt-1">{item.subject}</h4>
<p className="text-sm text-gray-500 mt-1 truncate">
<h4 className="font-medium text-sm mt-1">{item.subject}</h4>
<p className="text-xs text-gray-500 mt-1 truncate">
{item.text.substring(0, 100)}...
</p>
{item.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{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}
</span>
))}

View file

@ -2,59 +2,230 @@ import React, { useState } from 'react';
import { AccountSwitcher } from './account-switcher';
import { MailList } from './mail-list';
import { MailDisplay } from './mail-display';
import { Nav } from './nav';
import { mails, accounts } from '../data';
export function Mail() {
const [isCollapsed, setIsCollapsed] = useState(false);
const [selectedMailId, setSelectedMailId] = useState<string | null>(mails[0].id);
const [activeTab, setActiveTab] = useState('all');
const [selectedMailId, setSelectedMailId] = useState<string | null>(null);
const toggleCollapse = () => setIsCollapsed(!isCollapsed);
const filteredMails = activeTab === 'all'
? mails
: 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 (
<div className="flex h-screen">
<div className={`${isCollapsed ? 'w-16' : 'w-64'} border-r flex flex-col`}>
<div className="flex h-screen bg-white">
{/* 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">
<AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} />
</div>
<div className="p-4">
<button onClick={toggleCollapse} className="w-full text-left">
{isCollapsed ? '»' : '« Collapse'}
<div className="flex-1 overflow-auto py-2">
<Nav links={navItems} isCollapsed={isCollapsed} />
</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>
</div>
</div>
<div className="flex-1 flex flex-col border-r">
<div className="p-4 border-b">
{/* Mail list */}
<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">
<h2 className="text-xl font-bold">Inbox</h2>
<div className="flex space-x-2">
<button
onClick={() => setActiveTab('all')}
className={`px-3 py-1 rounded ${activeTab === 'all' ? 'bg-blue-500 text-white' : ''}`}
>
<h2 className="text-xl font-semibold">Inbox</h2>
<div className="flex space-x-1">
<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')}>
All mail
</button>
<button
onClick={() => setActiveTab('unread')}
className={`px-3 py-1 rounded ${activeTab === 'unread' ? 'bg-blue-500 text-white' : ''}`}
>
<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')}>
Unread
</button>
</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
type="text"
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 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">
<MailList
items={filteredMails}
@ -64,6 +235,7 @@ export function Mail() {
</div>
</div>
{/* Mail content */}
<div className="flex-1">
<MailDisplay mail={mails.find(mail => mail.id === selectedMailId) || null} />
</div>

View file

@ -12,18 +12,20 @@ interface NavProps {
export function Nav({ links, isCollapsed }: NavProps) {
return (
<div className={`flex ${isCollapsed ? 'flex-col items-center' : 'flex-col'}`}>
<div className="space-y-1 px-2">
{links.map((link, index) => (
<button
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">
{link.icon}
</span>
{!isCollapsed && (
<span className="ml-2">{link.title}</span>
<span>{link.title}</span>
)}
</div>
{!isCollapsed && link.label && (

View file

@ -1,3 +1,5 @@
import React from 'react';
export const mails = [
{
id: "6c84fb90-12c4-11e1-840d-7b25c5ee775a",
@ -19,7 +21,16 @@ export const mails = [
read: true,
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 = [

View file

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

View file

@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import RootLayout from ".";
import './styles/globals.css' // or your CSS file path
ReactDOM.createRoot(document.getElementById('root')).render(
<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": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
@ -9,9 +13,6 @@
],
"module": "ESNext",
"skipLibCheck": true,
"types": [
"@tauri-apps/api/tauri"
],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,

View file

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