refactor: update component exports to named exports and enhance UI structure
This commit is contained in:
parent
4dc77b7717
commit
661b821f37
33 changed files with 792 additions and 3067 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -22,3 +22,4 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
output.sh
|
|
@ -1,63 +1,33 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export function CalendarDateRangePicker() {
|
||||
const [dateRange, setDateRange] = useState({ startDate: new Date(), endDate: new Date() });
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
|
||||
const onChangeStart = (event, selectedDate) => {
|
||||
setShowStartPicker(false);
|
||||
if (selectedDate) {
|
||||
setDateRange(prev => ({ ...prev, startDate: selectedDate }));
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeEnd = (event, selectedDate) => {
|
||||
setShowEndPicker(false);
|
||||
if (selectedDate) {
|
||||
setDateRange(prev => ({ ...prev, endDate: selectedDate }));
|
||||
}
|
||||
};
|
||||
const [dateRange, setDateRange] = useState({
|
||||
startDate: new Date(),
|
||||
endDate: new Date()
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity onPress={() => setShowStartPicker(true)} style={styles.button}>
|
||||
<Text>Start: {dateRange.startDate.toLocaleDateString()}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setShowEndPicker(true)} style={styles.button}>
|
||||
<Text>End: {dateRange.endDate.toLocaleDateString()}</Text>
|
||||
</TouchableOpacity>
|
||||
{showStartPicker && (
|
||||
<DateTimePicker
|
||||
value={dateRange.startDate}
|
||||
mode="date"
|
||||
display="default"
|
||||
onChange={onChangeStart}
|
||||
/>
|
||||
)}
|
||||
{showEndPicker && (
|
||||
<DateTimePicker
|
||||
value={dateRange.endDate}
|
||||
mode="date"
|
||||
display="default"
|
||||
onChange={onChangeEnd}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="px-3 py-1 border rounded"
|
||||
onClick={() => {
|
||||
const date = new Date(prompt("Enter start date (YYYY-MM-DD)") || dateRange.startDate);
|
||||
setDateRange(prev => ({ ...prev, startDate: date }));
|
||||
}}
|
||||
>
|
||||
Start: {format(dateRange.startDate, 'MMM dd, yyyy')}
|
||||
</button>
|
||||
<span>to</span>
|
||||
<button
|
||||
className="px-3 py-1 border rounded"
|
||||
onClick={() => {
|
||||
const date = new Date(prompt("Enter end date (YYYY-MM-DD)") || dateRange.endDate);
|
||||
setDateRange(prev => ({ ...prev, endDate: date }));
|
||||
}}
|
||||
>
|
||||
End: {format(dateRange.endDate, 'MMM dd, yyyy')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginVertical: 10,
|
||||
},
|
||||
button: {
|
||||
padding: 10,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 5,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,36 +1,16 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
export function MainNav() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<Text style={styles.navText}>Overview</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<Text style={styles.navText}>Customers</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<Text style={styles.navText}>Products</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<Text style={styles.navText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<nav className="flex space-x-4">
|
||||
{['Overview', 'Customers', 'Products', 'Settings'].map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
className="px-3 py-2 text-sm font-medium hover:text-primary"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
navItem: {
|
||||
padding: 10,
|
||||
},
|
||||
navText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,60 +1,50 @@
|
|||
import React from 'react';
|
||||
import { View, Dimensions, StyleSheet } from 'react-native';
|
||||
import { BarChart } from 'react-native-chart-kit';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
const data = {
|
||||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100,
|
||||
Math.random() * 100
|
||||
]
|
||||
}
|
||||
]
|
||||
datasets: [{
|
||||
label: 'Revenue',
|
||||
data: Array(12).fill(0).map(() => Math.random() * 100),
|
||||
backgroundColor: 'rgba(0, 119, 255, 0.5)',
|
||||
}]
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Monthly Revenue',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function Overview() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<BarChart
|
||||
data={data}
|
||||
width={Dimensions.get("window").width - 40}
|
||||
height={220}
|
||||
yAxisLabel="$"
|
||||
chartConfig={{
|
||||
backgroundColor: "#ffffff",
|
||||
backgroundGradientFrom: "#ffffff",
|
||||
backgroundGradientTo: "#ffffff",
|
||||
decimalPlaces: 2,
|
||||
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
|
||||
style: {
|
||||
borderRadius: 16
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
marginVertical: 8,
|
||||
borderRadius: 16
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<Bar options={options} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { Avatar, ListItem } from 'react-native-elements';
|
||||
|
||||
const salesData = [
|
||||
{ name: "Olivia Martin", email: "olivia.martin@email.com", amount: "+$1,999.00" },
|
||||
|
@ -12,20 +10,21 @@ const salesData = [
|
|||
|
||||
export function RecentSales() {
|
||||
return (
|
||||
<View>
|
||||
<div className="space-y-4">
|
||||
{salesData.map((item, index) => (
|
||||
<ListItem key={index} bottomDivider>
|
||||
<Avatar
|
||||
rounded
|
||||
source={{ uri: `https://i.pravatar.cc/150?u=${item.email}` }}
|
||||
/>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title>{item.name}</ListItem.Title>
|
||||
<ListItem.Subtitle>{item.email}</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
<Text>{item.amount}</Text>
|
||||
</ListItem>
|
||||
<div key={index} className="flex items-center justify-between p-2 border-b">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{item.name}</p>
|
||||
<p className="text-sm text-gray-500">{item.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-medium">{item.amount}</span>
|
||||
</div>
|
||||
))}
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,31 +1,21 @@
|
|||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Input } from 'react-native-elements';
|
||||
|
||||
export function Search() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Input
|
||||
<div className="relative max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
containerStyle={styles.inputContainer}
|
||||
inputContainerStyle={styles.inputInnerContainer}
|
||||
className="w-full pl-8 pr-4 py-2 rounded-full border focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</View>
|
||||
<svg
|
||||
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
maxWidth: 300,
|
||||
},
|
||||
inputContainer: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
inputInnerContainer: {
|
||||
borderBottomWidth: 0,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, Modal, StyleSheet } from 'react-native';
|
||||
import { Avatar, Button, Input } from 'react-native-elements';
|
||||
|
||||
const groups = [
|
||||
{
|
||||
|
@ -24,57 +22,83 @@ export function TeamSwitcher() {
|
|||
const [selectedTeam, setSelectedTeam] = useState(groups[0].teams[0]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity onPress={() => setOpen(true)}>
|
||||
<Avatar
|
||||
rounded
|
||||
source={{ uri: `https://avatar.vercel.sh/${selectedTeam.value}.png` }}
|
||||
/>
|
||||
<Text>{selectedTeam.label}</Text>
|
||||
</TouchableOpacity>
|
||||
<Modal visible={open} animationType="slide">
|
||||
<View style={styles.modal}>
|
||||
<Input placeholder="Search team..." />
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
{selectedTeam.label[0]}
|
||||
</div>
|
||||
<span>{selectedTeam.label}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute z-10 mt-2 w-56 bg-white rounded-md shadow-lg">
|
||||
<div className="p-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search team..."
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
{groups.map((group) => (
|
||||
<View key={group.label}>
|
||||
<Text style={styles.groupLabel}>{group.label}</Text>
|
||||
<div key={group.label} className="py-1">
|
||||
<p className="px-3 py-1 text-sm font-medium text-gray-500">{group.label}</p>
|
||||
{group.teams.map((team) => (
|
||||
<TouchableOpacity
|
||||
<button
|
||||
key={team.value}
|
||||
onPress={() => {
|
||||
onClick={() => {
|
||||
setSelectedTeam(team);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 hover:bg-gray-100"
|
||||
>
|
||||
<Text>{team.label}</Text>
|
||||
</TouchableOpacity>
|
||||
{team.label}
|
||||
</button>
|
||||
))}
|
||||
</View>
|
||||
</div>
|
||||
))}
|
||||
<Button title="Create Team" onPress={() => setShowNewTeamDialog(true)} />
|
||||
<Button title="Close" onPress={() => setOpen(false)} />
|
||||
</View>
|
||||
</Modal>
|
||||
<Modal visible={showNewTeamDialog} animationType="slide">
|
||||
<View style={styles.modal}>
|
||||
<Text>Create team</Text>
|
||||
<Input placeholder="Team name" />
|
||||
<Button title="Create" onPress={() => setShowNewTeamDialog(false)} />
|
||||
<Button title="Cancel" onPress={() => setShowNewTeamDialog(false)} />
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
<div className="border-t p-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setShowNewTeamDialog(true);
|
||||
}}
|
||||
className="w-full p-2 text-left hover:bg-gray-100"
|
||||
>
|
||||
Create Team
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNewTeamDialog && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="bg-white p-4 rounded-md">
|
||||
<h3 className="text-lg font-medium mb-4">Create team</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Team name"
|
||||
className="w-full p-2 border rounded mb-4"
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => setShowNewTeamDialog(false)}
|
||||
className="px-4 py-2 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowNewTeamDialog(false)}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modal: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
groupLabel: {
|
||||
fontWeight: 'bold',
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,57 +1,33 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Avatar, Overlay } from 'react-native-elements';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export function UserNav() {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
const toggleOverlay = () => {
|
||||
setVisible(!visible);
|
||||
};
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity onPress={toggleOverlay}>
|
||||
<Avatar
|
||||
rounded
|
||||
source={{ uri: 'https://i.pravatar.cc/150?u=a042581f4e29026704d' }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Overlay isVisible={visible} onBackdropPress={toggleOverlay}>
|
||||
<View>
|
||||
<Text style={styles.overlayText}>shadcn</Text>
|
||||
<Text style={styles.overlaySubtext}>m@example.com</Text>
|
||||
<TouchableOpacity style={styles.overlayButton}>
|
||||
<Text>Profile</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.overlayButton}>
|
||||
<Text>Billing</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.overlayButton}>
|
||||
<Text>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.overlayButton}>
|
||||
<Text>New Team</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.overlayButton}>
|
||||
<Text>Log out</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Overlay>
|
||||
</View>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center"
|
||||
>
|
||||
U
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10">
|
||||
<div className="p-2 border-b">
|
||||
<p className="font-medium">shadcn</p>
|
||||
<p className="text-sm text-gray-500">m@example.com</p>
|
||||
</div>
|
||||
{['Profile', 'Billing', 'Settings', 'New Team', 'Log out'].map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
className="w-full text-left px-3 py-2 hover:bg-gray-100"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlayText: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
overlaySubtext: {
|
||||
fontSize: 14,
|
||||
color: 'gray',
|
||||
},
|
||||
overlayButton: {
|
||||
paddingVertical: 10,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React from 'react';
|
||||
import { View, Text, ScrollView, StyleSheet } from 'react-native';
|
||||
import { Button, Card } from 'react-native-elements';
|
||||
import { TeamSwitcher } from './components/TeamSwitcher';
|
||||
import { MainNav } from './components/MainNav';
|
||||
import { Search } from './components/Search';
|
||||
|
@ -11,71 +9,56 @@ import { RecentSales } from './components/RecentSales';
|
|||
|
||||
export default function DashboardScreen() {
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TeamSwitcher />
|
||||
<MainNav />
|
||||
<View style={styles.rightHeader}>
|
||||
<Search />
|
||||
<UserNav />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.title}>Dashboard</Text>
|
||||
<View style={styles.actions}>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white border-b">
|
||||
<div className="container flex items-center justify-between h-16 px-4">
|
||||
<TeamSwitcher />
|
||||
<MainNav />
|
||||
<div className="flex items-center space-x-4">
|
||||
<Search />
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CalendarDateRangePicker />
|
||||
<Button title="Download" />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.cards}>
|
||||
<Card>
|
||||
<Card.Title>Total Revenue</Card.Title>
|
||||
<Text style={styles.cardValue}>$45,231.89</Text>
|
||||
<Text style={styles.cardSubtext}>+20.1% from last month</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title>Subscriptions</Card.Title>
|
||||
<Text style={styles.cardValue}>+2350</Text>
|
||||
<Text style={styles.cardSubtext}>+180.1% from last month</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title>Sales</Card.Title>
|
||||
<Text style={styles.cardValue}>+12,234</Text>
|
||||
<Text style={styles.cardSubtext}>+19% from last month</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title>Active Now</Card.Title>
|
||||
<Text style={styles.cardValue}>+573</Text>
|
||||
<Text style={styles.cardSubtext}>+201 since last hour</Text>
|
||||
</Card>
|
||||
</View>
|
||||
<View style={styles.charts}>
|
||||
<Card>
|
||||
<Card.Title>Overview</Card.Title>
|
||||
<button className="px-4 py-2 bg-blue-500 text-white rounded">
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[
|
||||
{ title: "Total Revenue", value: "$45,231.89", subtext: "+20.1% from last month" },
|
||||
{ title: "Subscriptions", value: "+2350", subtext: "+180.1% from last month" },
|
||||
{ title: "Sales", value: "+12,234", subtext: "+19% from last month" },
|
||||
{ title: "Active Now", value: "+573", subtext: "+201 since last hour" },
|
||||
].map((card, index) => (
|
||||
<div key={index} className="p-6 bg-white border rounded-lg">
|
||||
<h3 className="text-sm font-medium text-gray-500">{card.title}</h3>
|
||||
<p className="text-2xl font-bold mt-1">{card.value}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{card.subtext}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="p-6 bg-white border rounded-lg">
|
||||
<h3 className="text-lg font-medium mb-4">Overview</h3>
|
||||
<Overview />
|
||||
</Card>
|
||||
<Card>
|
||||
<Card.Title>Recent Sales</Card.Title>
|
||||
<Text>You made 265 sales this month.</Text>
|
||||
</div>
|
||||
<div className="p-6 bg-white border rounded-lg space-y-4">
|
||||
<h3 className="text-lg font-medium">Recent Sales</h3>
|
||||
<p>You made 265 sales this month.</p>
|
||||
<RecentSales />
|
||||
</Card>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor:'white' },
|
||||
header: { flexDirection: 'row', padding: 16, alignItems: 'center' },
|
||||
rightHeader: { flexDirection: 'row', marginLeft: 'auto' },
|
||||
content: { padding: 16 },
|
||||
titleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
actions: { flexDirection: 'row' },
|
||||
cards: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
|
||||
charts: { marginTop: 16 },
|
||||
cardValue: { fontSize: 20, fontWeight: 'bold' },
|
||||
cardSubtext: { fontSize: 12, color: 'gray' },
|
||||
});
|
||||
|
|
|
@ -7,7 +7,12 @@ interface FileItem {
|
|||
is_dir: boolean;
|
||||
}
|
||||
|
||||
const FileBrowser = ({ path }: { path: string }) => {
|
||||
interface FileBrowserProps {
|
||||
path: string;
|
||||
onFileSelect?: (path: string) => void;
|
||||
}
|
||||
|
||||
export function FileBrowser({ path, onFileSelect }: FileBrowserProps) {
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
@ -34,7 +39,11 @@ const FileBrowser = ({ path }: { path: string }) => {
|
|||
) : (
|
||||
<ul className="space-y-1">
|
||||
{files.map((file) => (
|
||||
<li key={file.path} className="p-2 hover:bg-gray-100 rounded">
|
||||
<li
|
||||
key={file.path}
|
||||
className={`p-2 hover:bg-gray-100 rounded cursor-pointer ${file.is_dir ? 'font-medium' : ''}`}
|
||||
onClick={() => onFileSelect && onFileSelect(file.path)}
|
||||
>
|
||||
{file.is_dir ? '📁' : '📄'} {file.name}
|
||||
</li>
|
||||
))}
|
||||
|
@ -42,6 +51,4 @@ const FileBrowser = ({ path }: { path: string }) => {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileBrowser;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { writeBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface FileOperationsProps {
|
||||
|
@ -8,7 +7,7 @@ interface FileOperationsProps {
|
|||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const FileOperations = ({ currentPath, onRefresh }: FileOperationsProps) => {
|
||||
export function FileOperations({ currentPath, onRefresh }: FileOperationsProps) {
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
const handleUpload = async () => {
|
||||
|
@ -79,6 +78,4 @@ const FileOperations = ({ currentPath, onRefresh }: FileOperationsProps) => {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileOperations;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ interface FileItem {
|
|||
is_dir: boolean;
|
||||
}
|
||||
|
||||
const FileTree = ({ onSelect }: { onSelect: (path: string) => void }) => {
|
||||
interface FileTreeProps {
|
||||
onSelect: (path: string) => void;
|
||||
}
|
||||
|
||||
export function FileTree({ onSelect }: FileTreeProps) {
|
||||
const [tree, setTree] = useState<FileItem[]>([]);
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
|
||||
|
@ -43,7 +47,7 @@ const FileTree = ({ onSelect }: { onSelect: (path: string) => void }) => {
|
|||
</div>
|
||||
{expanded[item.path] && (
|
||||
<div className="pl-4">
|
||||
<FileBrowser path={item.path} />
|
||||
<FileBrowser path={item.path} onSelect={onSelect} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
|
@ -51,6 +55,4 @@ const FileTree = ({ onSelect }: { onSelect: (path: string) => void }) => {
|
|||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileTree;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useState } from 'react';
|
||||
import FileTree from './components/FileTree';
|
||||
import FileBrowser from './components/FileBrowser';
|
||||
import FileOperations from './components/FileOperations';
|
||||
import { FileTree } from './components/FileTree';
|
||||
import { FileBrowser } from './components/FileBrowser';
|
||||
import { FileOperations } from './components/FileOperations';
|
||||
|
||||
const DriveScreen = () => {
|
||||
export function DriveScreen() {
|
||||
const [currentPath, setCurrentPath] = useState('');
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
|
@ -20,6 +20,4 @@ const DriveScreen = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DriveScreen;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, Modal, FlatList } from 'react-native';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
interface Account {
|
||||
email: string;
|
||||
|
@ -13,101 +11,51 @@ interface AccountSwitcherProps {
|
|||
accounts: Account[];
|
||||
}
|
||||
|
||||
function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps) {
|
||||
export function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps) {
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>(accounts[0].email);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const selectedAccountData = accounts.find((account) => account.email === selectedAccount);
|
||||
|
||||
const renderItem = ({ item }: { item: Account }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.accountItem}
|
||||
onPress={() => {
|
||||
setSelectedAccount(item.email);
|
||||
setModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<View style={styles.iconContainer}>{item.icon}</View>
|
||||
<Text style={styles.accountEmail}>{item.email}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={[styles.selectTrigger, isCollapsed && styles.collapsedTrigger]}
|
||||
onPress={() => setModalVisible(true)}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-2'}`}
|
||||
>
|
||||
{selectedAccountData && (
|
||||
<>
|
||||
<View style={styles.iconContainer}>{selectedAccountData.icon}</View>
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
{selectedAccountData.icon}
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<Text style={styles.selectedAccountLabel}>{selectedAccountData.label}</Text>
|
||||
<span>{selectedAccountData.label}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modalContent}>
|
||||
<FlatList
|
||||
data={accounts}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.email}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 mt-2 w-56 bg-white rounded-md shadow-lg">
|
||||
<div className="p-2">
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
key={account.email}
|
||||
onClick={() => {
|
||||
setSelectedAccount(account.email);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 hover:bg-gray-100 flex items-center space-x-2"
|
||||
>
|
||||
<div className="w-5 h-5 flex items-center justify-center">
|
||||
{account.icon}
|
||||
</div>
|
||||
<span>{account.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
selectTrigger: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 5,
|
||||
},
|
||||
collapsedTrigger: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: 10,
|
||||
},
|
||||
selectedAccountLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
width: '80%',
|
||||
maxHeight: '80%',
|
||||
},
|
||||
accountItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
accountEmail: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default AccountSwitcher;
|
||||
|
|
|
@ -1,80 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, TextInput, StyleSheet, ScrollView, Modal } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { Switch } from 'react-native';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// Simple Avatar component
|
||||
const Avatar = ({ name }: { name: string }) => (
|
||||
<View style={styles.avatar}>
|
||||
<Text style={styles.avatarText}>{name[0]}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const formatDistanceToNow = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return 'just now';
|
||||
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
||||
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
||||
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
||||
if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} months ago`;
|
||||
return `${Math.floor(diffInSeconds / 31536000)} years ago`;
|
||||
};
|
||||
|
||||
// Simple Separator component
|
||||
const Separator = () => <View style={styles.separator} />;
|
||||
|
||||
// Simple Tooltip component (placeholder)
|
||||
const Tooltip = ({ content, children }: { content: string; children: React.ReactNode }) => (
|
||||
<View>
|
||||
{children}
|
||||
{/* Implement tooltip logic here */}
|
||||
</View>
|
||||
);
|
||||
|
||||
// Simple Calendar component (placeholder)
|
||||
const Calendar = () => (
|
||||
<View>
|
||||
<Text>Calendar Placeholder</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to add hours to a date
|
||||
const addHours = (date: Date, hours: number) => {
|
||||
const newDate = new Date(date);
|
||||
newDate.setHours(date.getHours() + hours);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
// Helper function to add days to a date
|
||||
const addDays = (date: Date, days: number) => {
|
||||
const newDate = new Date(date);
|
||||
newDate.setDate(date.getDate() + days);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
// Helper function to get next Saturday
|
||||
const nextSaturday = (date: Date) => {
|
||||
const newDate = new Date(date);
|
||||
newDate.setDate(date.getDate() + ((6 - date.getDay() + 7) % 7));
|
||||
return newDate;
|
||||
};
|
||||
|
||||
// Define Mail type
|
||||
interface Mail {
|
||||
id: string;
|
||||
name: string;
|
||||
subject: string;
|
||||
email: string;
|
||||
|
@ -88,255 +16,53 @@ interface MailDisplayProps {
|
|||
|
||||
export function MailDisplay({ mail }: MailDisplayProps) {
|
||||
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
|
||||
const today = new Date();
|
||||
|
||||
const renderToolbarButton = (icon: string, label: string, onPress: () => void) => (
|
||||
<Tooltip content={label}>
|
||||
<TouchableOpacity style={styles.toolbarButton} onPress={onPress} disabled={!mail}>
|
||||
<Icon name={icon} size={16} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
);
|
||||
if (!mail) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p>No message selected</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.toolbar}>
|
||||
<View style={styles.toolbarGroup}>
|
||||
{renderToolbarButton('archive', 'Archive', () => {})}
|
||||
{renderToolbarButton('x-square', 'Move to junk', () => {})}
|
||||
{renderToolbarButton('trash-2', 'Move to trash', () => {})}
|
||||
<Separator />
|
||||
<TouchableOpacity
|
||||
style={styles.toolbarButton}
|
||||
onPress={() => setIsPopoverVisible(true)}
|
||||
disabled={!mail}
|
||||
>
|
||||
<Icon name="clock" size={16} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.toolbarGroup}>
|
||||
{renderToolbarButton('reply', 'Reply', () => {})}
|
||||
{renderToolbarButton('reply-all', 'Reply all', () => {})}
|
||||
{renderToolbarButton('corner-up-right', 'Forward', () => {})}
|
||||
</View>
|
||||
<Separator />
|
||||
<TouchableOpacity style={styles.toolbarButton} disabled={!mail}>
|
||||
<Icon name="more-vertical" size={16} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Separator />
|
||||
{mail ? (
|
||||
<ScrollView style={styles.mailContent}>
|
||||
<View style={styles.mailHeader}>
|
||||
<View style={styles.mailSender}>
|
||||
<Avatar name={mail.name} />
|
||||
<View style={styles.mailSenderInfo}>
|
||||
<Text style={styles.mailSenderName}>{mail.name}</Text>
|
||||
<Text style={styles.mailSubject}>{mail.subject}</Text>
|
||||
<Text style={styles.mailReplyTo}>
|
||||
<Text style={styles.mailReplyToLabel}>Reply-To:</Text> {mail.email}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{mail.date && (
|
||||
<Text style={styles.mailDate}>
|
||||
{formatDate(new Date(mail.date))}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Separator />
|
||||
<Text style={styles.mailBody}>{mail.text}</Text>
|
||||
<Separator />
|
||||
<View style={styles.replyForm}>
|
||||
<TextInput
|
||||
style={styles.replyInput}
|
||||
multiline
|
||||
placeholder={`Reply ${mail.name}...`}
|
||||
/>
|
||||
<View style={styles.replyFormFooter}>
|
||||
<View style={styles.muteThreadContainer}>
|
||||
<Switch />
|
||||
<Text style={styles.muteThreadLabel}>Mute this thread</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.sendButton}>
|
||||
<Text style={styles.sendButtonText}>Send</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View style={styles.noMailSelected}>
|
||||
<Text>No message selected</Text>
|
||||
</View>
|
||||
)}
|
||||
<Modal
|
||||
visible={isPopoverVisible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setIsPopoverVisible(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.modalTitle}>Snooze until</Text>
|
||||
<TouchableOpacity style={styles.snoozeOption}>
|
||||
<Text>Later today</Text>
|
||||
<Text style={styles.snoozeTime}>
|
||||
{formatDate(addHours(today, 4))}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.snoozeOption}>
|
||||
<Text>Tomorrow</Text>
|
||||
<Text style={styles.snoozeTime}>
|
||||
{formatDate(addDays(today, 1))}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.snoozeOption}>
|
||||
<Text>This weekend</Text>
|
||||
<Text style={styles.snoozeTime}>
|
||||
{formatDate(nextSaturday(today))}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.snoozeOption}>
|
||||
<Text>Next week</Text>
|
||||
<Text style={styles.snoozeTime}>
|
||||
{formatDate(addDays(today, 7))}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Calendar />
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
<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">
|
||||
{mail.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">{mail.name}</h3>
|
||||
<p className="text-sm text-gray-500">{mail.subject}</p>
|
||||
<p className="text-xs text-gray-500">Reply-To: {mail.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{format(new Date(mail.date), 'MMM d, yyyy h:mm a')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<p className="whitespace-pre-line">{mail.text}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t">
|
||||
<textarea
|
||||
placeholder={`Reply ${mail.name}...`}
|
||||
className="w-full p-2 border rounded"
|
||||
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>
|
||||
</label>
|
||||
<button className="px-4 py-2 bg-blue-500 text-white rounded">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
toolbar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 8,
|
||||
},
|
||||
toolbarGroup: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
toolbarButton: {
|
||||
padding: 8,
|
||||
},
|
||||
mailContent: {
|
||||
flex: 1,
|
||||
},
|
||||
mailHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
},
|
||||
mailSender: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
mailSenderInfo: {
|
||||
marginLeft: 16,
|
||||
},
|
||||
mailSenderName: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
mailSubject: {
|
||||
fontSize: 12,
|
||||
},
|
||||
mailReplyTo: {
|
||||
fontSize: 12,
|
||||
},
|
||||
mailReplyToLabel: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
mailDate: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
mailBody: {
|
||||
padding: 16,
|
||||
fontSize: 14,
|
||||
},
|
||||
replyForm: {
|
||||
padding: 16,
|
||||
},
|
||||
replyInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
minHeight: 100,
|
||||
},
|
||||
replyFormFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
muteThreadContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
muteThreadLabel: {
|
||||
marginLeft: 8,
|
||||
fontSize: 12,
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: '#007AFF',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
sendButtonText: {
|
||||
color: '#fff',
|
||||
},
|
||||
noMailSelected: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: '#fff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
width: '80%',
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
},
|
||||
snoozeOption: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
snoozeTime: {
|
||||
color: '#666',
|
||||
},
|
||||
avatar: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#ccc',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatarText: {
|
||||
fontSize: 18,
|
||||
color: '#fff',
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: '#e0e0e0',
|
||||
marginVertical: 8,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,147 +1,56 @@
|
|||
import React from "react";
|
||||
import { View, Text, TouchableOpacity, FlatList, StyleSheet } from "react-native";
|
||||
import React from 'react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
import { Mail } from "../data";
|
||||
import { useMail } from "../use-mail";
|
||||
interface Mail {
|
||||
id: string;
|
||||
name: string;
|
||||
subject: string;
|
||||
email: string;
|
||||
date: string;
|
||||
text: string;
|
||||
read: boolean;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
interface MailListProps {
|
||||
items: Mail[];
|
||||
selectedId?: string;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export function MailList({ items }: MailListProps) {
|
||||
const [mail, setMail] = useMail();
|
||||
|
||||
const formatDistanceToNow = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return 'just now';
|
||||
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
||||
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
||||
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
||||
if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} months ago`;
|
||||
return `${Math.floor(diffInSeconds / 31536000)} years ago`;
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: Mail }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.mailItem,
|
||||
mail.selected === item.id && styles.selectedMailItem,
|
||||
]}
|
||||
onPress={() =>
|
||||
setMail({
|
||||
...mail,
|
||||
selected: item.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={styles.mailHeader}>
|
||||
<View style={styles.nameContainer}>
|
||||
<Text style={styles.name}>{item.name}</Text>
|
||||
{!item.read && <View style={styles.unreadDot} />}
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.date,
|
||||
mail.selected === item.id ? styles.selectedDate : styles.mutedDate,
|
||||
]}
|
||||
>
|
||||
{formatDistanceToNow(new Date(item.date))}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.subject}>{item.subject}</Text>
|
||||
<Text style={styles.preview} numberOfLines={2}>
|
||||
{item.text.substring(0, 300)}
|
||||
</Text>
|
||||
{item.labels.length > 0 && (
|
||||
<View style={styles.labelContainer}>
|
||||
{item.labels.map((label) => (
|
||||
<View key={label} style={styles.label}>
|
||||
<Text style={styles.labelText}>{label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
export function MailList({ items, selectedId, onSelect }: MailListProps) {
|
||||
return (
|
||||
<FlatList
|
||||
data={items}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.container}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
className={`p-4 border rounded cursor-pointer ${selectedId === item.id ? 'bg-gray-100' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-2 h-2 rounded-full ${item.read ? 'bg-transparent' : 'bg-blue-500'}`} />
|
||||
<h3 className="font-medium">{item.name}</h3>
|
||||
</div>
|
||||
<span className="text-sm 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">
|
||||
{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">
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
},
|
||||
mailItem: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
selectedMailItem: {
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
mailHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
nameContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
name: {
|
||||
fontWeight: 'bold',
|
||||
marginRight: 8,
|
||||
},
|
||||
unreadDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'blue',
|
||||
},
|
||||
date: {
|
||||
fontSize: 12,
|
||||
},
|
||||
selectedDate: {
|
||||
color: '#000',
|
||||
},
|
||||
mutedDate: {
|
||||
color: '#666',
|
||||
},
|
||||
subject: {
|
||||
fontWeight: '500',
|
||||
marginBottom: 4,
|
||||
},
|
||||
preview: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
marginBottom: 8,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
label: {
|
||||
backgroundColor: '#e0e0e0',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
marginRight: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
labelText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,146 +1,72 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
} from 'react-native';
|
||||
|
||||
|
||||
import { MailDisplay } from './mail-display';
|
||||
import { AccountSwitcher } from './account-switcher';
|
||||
import { MailList } from './mail-list';
|
||||
import { Nav } from './nav';
|
||||
import { useMail } from '../use-mail';
|
||||
import { MailDisplay } from './mail-display';
|
||||
import { mails, accounts } from '../data';
|
||||
|
||||
interface MailProps {
|
||||
accounts: {
|
||||
label: string;
|
||||
email: string;
|
||||
icon: React.ReactNode;
|
||||
}[];
|
||||
mails: [];
|
||||
}
|
||||
|
||||
export function Mail({ accounts, mails }: MailProps) {
|
||||
export function Mail() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [mail] = useMail();
|
||||
const [selectedMailId, setSelectedMailId] = useState<string | null>(null);
|
||||
|
||||
const toggleCollapse = () => setIsCollapsed(!isCollapsed);
|
||||
|
||||
const filteredMails = activeTab === 'all'
|
||||
? mails
|
||||
: mails.filter(mail => !mail.read);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.sidebar, isCollapsed && styles.collapsedSidebar]}>
|
||||
<TouchableOpacity onPress={toggleCollapse} style={styles.collapseButton}>
|
||||
|
||||
</TouchableOpacity>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Inbox</Text>
|
||||
<View style={styles.tabs}>
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, activeTab === 'all' && styles.activeTab]}
|
||||
onPress={() => setActiveTab('all')}
|
||||
>
|
||||
<Text>All mail</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, activeTab === 'unread' && styles.activeTab]}
|
||||
onPress={() => setActiveTab('unread')}
|
||||
>
|
||||
<Text>Unread</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.searchContainer}>
|
||||
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="gray"
|
||||
<div className="flex h-screen">
|
||||
<div className={`${isCollapsed ? 'w-16' : 'w-64'} border-r flex flex-col`}>
|
||||
<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'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col border-r">
|
||||
<div className="p-4 border-b">
|
||||
<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' : ''}`}
|
||||
>
|
||||
All mail
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('unread')}
|
||||
className={`px-3 py-1 rounded ${activeTab === 'unread' ? 'bg-blue-500 text-white' : ''}`}
|
||||
>
|
||||
Unread
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<MailList
|
||||
items={filteredMails}
|
||||
selectedId={selectedMailId}
|
||||
onSelect={setSelectedMailId}
|
||||
/>
|
||||
</View>
|
||||
<ScrollView>
|
||||
<MailList
|
||||
items={activeTab === 'all' ? mails : mails.filter((item) => !item.read)}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<View style={styles.mailDisplay}>
|
||||
<MailDisplay
|
||||
mail={mails.find((item) => item.id === mail.selected) || null}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<MailDisplay mail={mails.find(mail => mail.id === selectedMailId) || null} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sidebar: {
|
||||
width: 250,
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
collapsedSidebar: {
|
||||
width: 50,
|
||||
},
|
||||
collapseButton: {
|
||||
padding: 10,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e0e0e0',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tab: {
|
||||
padding: 10,
|
||||
},
|
||||
activeTab: {
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: 'blue',
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f0f0f0',
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
searchIcon: {
|
||||
padding: 10,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
},
|
||||
mailDisplay: {
|
||||
width: '40%',
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: '#e0e0e0',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,102 +1,36 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { LucideIcon } from 'lucide-react-native';
|
||||
|
||||
interface NavProps {
|
||||
isCollapsed: boolean;
|
||||
links: {
|
||||
title: string;
|
||||
label?: string;
|
||||
icon: LucideIcon;
|
||||
icon: React.ReactNode;
|
||||
variant: 'default' | 'ghost';
|
||||
}[];
|
||||
}
|
||||
|
||||
export function Nav({ links, isCollapsed }: NavProps) {
|
||||
return (
|
||||
<View style={[styles.container, isCollapsed && styles.collapsedContainer]}>
|
||||
<View style={styles.nav}>
|
||||
{links.map((link, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[
|
||||
styles.link,
|
||||
isCollapsed ? styles.collapsedLink : styles.expandedLink,
|
||||
link.variant === 'default' ? styles.defaultVariant : styles.ghostVariant,
|
||||
]}
|
||||
onPress={() => {/* Add navigation logic here */}}
|
||||
>
|
||||
<link.icon
|
||||
style={[
|
||||
styles.icon,
|
||||
isCollapsed ? styles.collapsedIcon : styles.expandedIcon,
|
||||
]}
|
||||
/>
|
||||
<div className={`flex ${isCollapsed ? 'flex-col items-center' : 'flex-col'}`}>
|
||||
{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'}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="w-5 h-5 flex items-center justify-center">
|
||||
{link.icon}
|
||||
</span>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<Text style={styles.title}>{link.title}</Text>
|
||||
{link.label && (
|
||||
<Text style={styles.label}>{link.label}</Text>
|
||||
)}
|
||||
</>
|
||||
<span className="ml-2">{link.title}</span>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</div>
|
||||
{!isCollapsed && link.label && (
|
||||
<span className="text-xs text-gray-500">{link.label}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
collapsedContainer: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
nav: {
|
||||
gap: 4,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
link: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
collapsedLink: {
|
||||
justifyContent: 'center',
|
||||
width: 36,
|
||||
height: 36,
|
||||
},
|
||||
expandedLink: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
defaultVariant: {
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
ghostVariant: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
icon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
collapsedIcon: {
|
||||
marginRight: 0,
|
||||
},
|
||||
expandedIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
marginLeft: 'auto',
|
||||
color: '#6b7280',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,159 +19,8 @@ export const mails = [
|
|||
read: true,
|
||||
labels: ["work", "important"],
|
||||
},
|
||||
{
|
||||
id: "3e7c3f6d-bdf5-46ae-8d90-171300f27ae2",
|
||||
name: "Bob Johnson",
|
||||
email: "bobjohnson@example.com",
|
||||
subject: "Weekend Plans",
|
||||
text: "Any plans for the weekend? I was thinking of going hiking in the nearby mountains. It's been a while since we had some outdoor fun.\n\nIf you're interested, let me know, and we can plan the details. It'll be a great way to unwind and enjoy nature.\n\nLooking forward to your response!\n\nBest, Bob",
|
||||
date: "2023-04-10T11:45:00",
|
||||
read: true,
|
||||
labels: ["personal"],
|
||||
},
|
||||
{
|
||||
id: "61c35085-72d7-42b4-8d62-738f700d4b92",
|
||||
name: "Emily Davis",
|
||||
email: "emilydavis@example.com",
|
||||
subject: "Re: Question about Budget",
|
||||
text: "I have a question about the budget for the upcoming project. It seems like there's a discrepancy in the allocation of resources.\n\nI've reviewed the budget report and identified a few areas where we might be able to optimize our spending without compromising the project's quality.\n\nI've attached a detailed analysis for your reference. Let's discuss this further in our next meeting.\n\nThanks, Emily",
|
||||
date: "2023-03-25T13:15:00",
|
||||
read: false,
|
||||
labels: ["work", "budget"],
|
||||
},
|
||||
{
|
||||
id: "8f7b5db9-d935-4e42-8e05-1f1d0a3dfb97",
|
||||
name: "Michael Wilson",
|
||||
email: "michaelwilson@example.com",
|
||||
subject: "Important Announcement",
|
||||
text: "I have an important announcement to make during our team meeting. It pertains to a strategic shift in our approach to the upcoming product launch. We've received valuable feedback from our beta testers, and I believe it's time to make some adjustments to better meet our customers' needs.\n\nThis change is crucial to our success, and I look forward to discussing it with the team. Please be prepared to share your insights during the meeting.\n\nRegards, Michael",
|
||||
date: "2023-03-10T15:00:00",
|
||||
read: false,
|
||||
labels: ["meeting", "work", "important"],
|
||||
},
|
||||
{
|
||||
id: "1f0f2c02-e299-40de-9b1d-86ef9e42126b",
|
||||
name: "Sarah Brown",
|
||||
email: "sarahbrown@example.com",
|
||||
subject: "Re: Feedback on Proposal",
|
||||
text: "Thank you for your feedback on the proposal. It looks great! I'm pleased to hear that you found it promising. The team worked diligently to address all the key points you raised, and I believe we now have a strong foundation for the project.\n\nI've attached the revised proposal for your review.\n\nPlease let me know if you have any further comments or suggestions. Looking forward to your response.\n\nBest regards, Sarah",
|
||||
date: "2023-02-15T16:30:00",
|
||||
read: true,
|
||||
labels: ["work"],
|
||||
},
|
||||
{
|
||||
id: "17c0a96d-4415-42b1-8b4f-764efab57f66",
|
||||
name: "David Lee",
|
||||
email: "davidlee@example.com",
|
||||
subject: "New Project Idea",
|
||||
text: "I have an exciting new project idea to discuss with you. It involves expanding our services to target a niche market that has shown considerable growth in recent months.\n\nI've prepared a detailed proposal outlining the potential benefits and the strategy for execution.\n\nThis project has the potential to significantly impact our business positively. Let's set up a meeting to dive into the details and determine if it aligns with our current goals.\n\nBest regards, David",
|
||||
date: "2023-01-28T17:45:00",
|
||||
read: false,
|
||||
labels: ["meeting", "work", "important"],
|
||||
},
|
||||
{
|
||||
id: "2f0130cb-39fc-44c4-bb3c-0a4337edaaab",
|
||||
name: "Olivia Wilson",
|
||||
email: "oliviawilson@example.com",
|
||||
subject: "Vacation Plans",
|
||||
text: "Let's plan our vacation for next month. What do you think? I've been thinking of visiting a tropical paradise, and I've put together some destination options.\n\nI believe it's time for us to unwind and recharge. Please take a look at the options and let me know your preferences.\n\nWe can start making arrangements to ensure a smooth and enjoyable trip.\n\nExcited to hear your thoughts! Olivia",
|
||||
date: "2022-12-20T18:30:00",
|
||||
read: true,
|
||||
labels: ["personal"],
|
||||
},
|
||||
{
|
||||
id: "de305d54-75b4-431b-adb2-eb6b9e546014",
|
||||
name: "James Martin",
|
||||
email: "jamesmartin@example.com",
|
||||
subject: "Re: Conference Registration",
|
||||
text: "I've completed the registration for the conference next month. The event promises to be a great networking opportunity, and I'm looking forward to attending the various sessions and connecting with industry experts.\n\nI've also attached the conference schedule for your reference.\n\nIf there are any specific topics or sessions you'd like me to explore, please let me know. It's an exciting event, and I'll make the most of it.\n\nBest regards, James",
|
||||
date: "2022-11-30T19:15:00",
|
||||
read: true,
|
||||
labels: ["work", "conference"],
|
||||
},
|
||||
{
|
||||
id: "7dd90c63-00f6-40f3-bd87-5060a24e8ee7",
|
||||
name: "Sophia White",
|
||||
email: "sophiawhite@example.com",
|
||||
subject: "Team Dinner",
|
||||
text: "Let's have a team dinner next week to celebrate our success. We've achieved some significant milestones, and it's time to acknowledge our hard work and dedication.\n\nI've made reservations at a lovely restaurant, and I'm sure it'll be an enjoyable evening.\n\nPlease confirm your availability and any dietary preferences. Looking forward to a fun and memorable dinner with the team!\n\nBest, Sophia",
|
||||
date: "2022-11-05T20:30:00",
|
||||
read: false,
|
||||
labels: ["meeting", "work"],
|
||||
},
|
||||
{
|
||||
id: "99a88f78-3eb4-4d87-87b7-7b15a49a0a05",
|
||||
name: "Daniel Johnson",
|
||||
email: "danieljohnson@example.com",
|
||||
subject: "Feedback Request",
|
||||
text: "I'd like your feedback on the latest project deliverables. We've made significant progress, and I value your input to ensure we're on the right track.\n\nI've attached the deliverables for your review, and I'm particularly interested in any areas where you think we can further enhance the quality or efficiency.\n\nYour feedback is invaluable, and I appreciate your time and expertise. Let's work together to make this project a success.\n\nRegards, Daniel",
|
||||
date: "2022-10-22T09:30:00",
|
||||
read: false,
|
||||
labels: ["work"],
|
||||
},
|
||||
{
|
||||
id: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
name: "Ava Taylor",
|
||||
email: "avataylor@example.com",
|
||||
subject: "Re: Meeting Agenda",
|
||||
text: "Here's the agenda for our meeting next week. I've included all the topics we need to cover, as well as time allocations for each.\n\nIf you have any additional items to discuss or any specific points to address, please let me know, and we can integrate them into the agenda.\n\nIt's essential that our meeting is productive and addresses all relevant matters.\n\nLooking forward to our meeting! Ava",
|
||||
date: "2022-10-10T10:45:00",
|
||||
read: true,
|
||||
labels: ["meeting", "work"],
|
||||
},
|
||||
{
|
||||
id: "c1a0ecb4-2540-49c5-86f8-21e5ce79e4e6",
|
||||
name: "William Anderson",
|
||||
email: "williamanderson@example.com",
|
||||
subject: "Product Launch Update",
|
||||
text: "The product launch is on track. I'll provide an update during our call. We've made substantial progress in the development and marketing of our new product.\n\nI'm excited to share the latest updates with you during our upcoming call. It's crucial that we coordinate our efforts to ensure a successful launch. Please come prepared with any questions or insights you may have.\n\nLet's make this product launch a resounding success!\n\nBest regards, William",
|
||||
date: "2022-09-20T12:00:00",
|
||||
read: false,
|
||||
labels: ["meeting", "work", "important"],
|
||||
},
|
||||
{
|
||||
id: "ba54eefd-4097-4949-99f2-2a9ae4d1a836",
|
||||
name: "Mia Harris",
|
||||
email: "miaharris@example.com",
|
||||
subject: "Re: Travel Itinerary",
|
||||
text: "I've received the travel itinerary. It looks great! Thank you for your prompt assistance in arranging the details. I've reviewed the schedule and the accommodations, and everything seems to be in order. I'm looking forward to the trip, and I'm confident it'll be a smooth and enjoyable experience.\n\nIf there are any specific activities or attractions you recommend at our destination, please feel free to share your suggestions.\n\nExcited for the trip! Mia",
|
||||
date: "2022-09-10T13:15:00",
|
||||
read: true,
|
||||
labels: ["personal", "travel"],
|
||||
},
|
||||
{
|
||||
id: "df09b6ed-28bd-4e0c-85a9-9320ec5179aa",
|
||||
name: "Ethan Clark",
|
||||
email: "ethanclark@example.com",
|
||||
subject: "Team Building Event",
|
||||
text: "Let's plan a team-building event for our department. Team cohesion and morale are vital to our success, and I believe a well-organized team-building event can be incredibly beneficial. I've done some research and have a few ideas for fun and engaging activities.\n\nPlease let me know your thoughts and availability. We want this event to be both enjoyable and productive.\n\nTogether, we'll strengthen our team and boost our performance.\n\nRegards, Ethan",
|
||||
date: "2022-08-25T15:30:00",
|
||||
read: false,
|
||||
labels: ["meeting", "work"],
|
||||
},
|
||||
{
|
||||
id: "d67c1842-7f8b-4b4b-9be1-1b3b1ab4611d",
|
||||
name: "Chloe Hall",
|
||||
email: "chloehall@example.com",
|
||||
subject: "Re: Budget Approval",
|
||||
text: "The budget has been approved. We can proceed with the project. I'm delighted to inform you that our budget proposal has received the green light from the finance department. This is a significant milestone, and it means we can move forward with the project as planned.\n\nI've attached the finalized budget for your reference. Let's ensure that we stay on track and deliver the project on time and within budget.\n\nIt's an exciting time for us! Chloe",
|
||||
date: "2022-08-10T16:45:00",
|
||||
read: true,
|
||||
labels: ["work", "budget"],
|
||||
},
|
||||
{
|
||||
id: "6c9a7f94-8329-4d70-95d3-51f68c186ae1",
|
||||
name: "Samuel Turner",
|
||||
email: "samuelturner@example.com",
|
||||
subject: "Weekend Hike",
|
||||
text: "Who's up for a weekend hike in the mountains? I've been craving some outdoor adventure, and a hike in the mountains sounds like the perfect escape. If you're up for the challenge, we can explore some scenic trails and enjoy the beauty of nature.\n\nI've done some research and have a few routes in mind.\n\nLet me know if you're interested, and we can plan the details.\n\nIt's sure to be a memorable experience! Samuel",
|
||||
date: "2022-07-28T17:30:00",
|
||||
read: false,
|
||||
labels: ["personal"],
|
||||
},
|
||||
]
|
||||
|
||||
export type Mail = (typeof mails)[number]
|
||||
// More mails...
|
||||
];
|
||||
|
||||
export const accounts = [
|
||||
{
|
||||
|
@ -197,104 +46,7 @@ export const accounts = [
|
|||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Alicia Koch",
|
||||
email: "alicia@me.com",
|
||||
icon: (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>iCloud</title>
|
||||
<path
|
||||
d="M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export type Account = (typeof accounts)[number]
|
||||
|
||||
export const contacts = [
|
||||
{
|
||||
name: "Emma Johnson",
|
||||
email: "emma.johnson@example.com",
|
||||
},
|
||||
{
|
||||
name: "Liam Wilson",
|
||||
email: "liam.wilson@example.com",
|
||||
},
|
||||
{
|
||||
name: "Olivia Davis",
|
||||
email: "olivia.davis@example.com",
|
||||
},
|
||||
{
|
||||
name: "Noah Martinez",
|
||||
email: "noah.martinez@example.com",
|
||||
},
|
||||
{
|
||||
name: "Ava Taylor",
|
||||
email: "ava.taylor@example.com",
|
||||
},
|
||||
{
|
||||
name: "Lucas Brown",
|
||||
email: "lucas.brown@example.com",
|
||||
},
|
||||
{
|
||||
name: "Sophia Smith",
|
||||
email: "sophia.smith@example.com",
|
||||
},
|
||||
{
|
||||
name: "Ethan Wilson",
|
||||
email: "ethan.wilson@example.com",
|
||||
},
|
||||
{
|
||||
name: "Isabella Jackson",
|
||||
email: "isabella.jackson@example.com",
|
||||
},
|
||||
{
|
||||
name: "Mia Clark",
|
||||
email: "mia.clark@example.com",
|
||||
},
|
||||
{
|
||||
name: "Mason Lee",
|
||||
email: "mason.lee@example.com",
|
||||
},
|
||||
{
|
||||
name: "Layla Harris",
|
||||
email: "layla.harris@example.com",
|
||||
},
|
||||
{
|
||||
name: "William Anderson",
|
||||
email: "william.anderson@example.com",
|
||||
},
|
||||
{
|
||||
name: "Ella White",
|
||||
email: "ella.white@example.com",
|
||||
},
|
||||
{
|
||||
name: "James Thomas",
|
||||
email: "james.thomas@example.com",
|
||||
},
|
||||
{
|
||||
name: "Harper Lewis",
|
||||
email: "harper.lewis@example.com",
|
||||
},
|
||||
{
|
||||
name: "Benjamin Moore",
|
||||
email: "benjamin.moore@example.com",
|
||||
},
|
||||
{
|
||||
name: "Aria Hall",
|
||||
email: "aria.hall@example.com",
|
||||
},
|
||||
{
|
||||
name: "Henry Turner",
|
||||
email: "henry.turner@example.com",
|
||||
},
|
||||
{
|
||||
name: "Scarlett Adams",
|
||||
email: "scarlett.adams@example.com",
|
||||
},
|
||||
]
|
||||
|
||||
export type Contact = (typeof contacts)[number]
|
||||
export type Mail = typeof mails[0];
|
||||
export type Account = typeof accounts[0];
|
||||
|
|
|
@ -1,52 +1,10 @@
|
|||
import React from 'react';
|
||||
import { View, Image, StyleSheet, useColorScheme } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
import { accounts, mails } from './data';
|
||||
import { Mail } from './components/mail';
|
||||
|
||||
const MailPage = () => {
|
||||
const [defaultLayout, setDefaultLayout] = React.useState(undefined);
|
||||
const [defaultCollapsed, setDefaultCollapsed] = React.useState(undefined);
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
const loadStorageData = async () => {
|
||||
try {
|
||||
const layoutData = await AsyncStorage.getItem('react-resizable-panels:layout:mail');
|
||||
const collapsedData = await AsyncStorage.getItem('react-resizable-panels:collapsed');
|
||||
|
||||
setDefaultLayout(layoutData ? JSON.parse(layoutData) : undefined);
|
||||
setDefaultCollapsed(collapsedData ? JSON.parse(collapsedData) : undefined);
|
||||
} catch (error) {
|
||||
console.error('Error loading storage data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadStorageData();
|
||||
}, []);
|
||||
|
||||
export function MailPage() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Mail
|
||||
accounts={accounts}
|
||||
mails={mails}
|
||||
defaultLayout={defaultLayout}
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
navCollapsedSize={4}
|
||||
/>
|
||||
</View>
|
||||
<div className="h-screen">
|
||||
<Mail />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
},
|
||||
});
|
||||
|
||||
export default MailPage;
|
||||
}
|
||||
|
|
|
@ -1,43 +1,9 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
import { Mail, mails } from './data'; // Update the import path as needed
|
||||
|
||||
type Config = {
|
||||
selected: Mail['id'] | null;
|
||||
};
|
||||
|
||||
const initialConfig: Config = {
|
||||
selected: mails[0].id,
|
||||
};
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useMail() {
|
||||
const [config, setConfig] = useState<Config>(initialConfig);
|
||||
const [mail, setMail] = useState({
|
||||
selected: mails[0].id,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Load config from AsyncStorage when component mounts
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const storedConfig = await AsyncStorage.getItem('mailConfig');
|
||||
if (storedConfig) {
|
||||
setConfig(JSON.parse(storedConfig));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading mail config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = async (newConfig: Config) => {
|
||||
try {
|
||||
await AsyncStorage.setItem('mailConfig', JSON.stringify(newConfig));
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.error('Error saving mail config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return [config, updateConfig] as const;
|
||||
return [mail, setMail] as const;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ interface SyncStatus {
|
|||
|
||||
type Screen = "Main" | "Status";
|
||||
|
||||
function App() {
|
||||
function Page() {
|
||||
const [state, setState] = useState({
|
||||
name: "",
|
||||
access_key: "",
|
||||
|
@ -200,4 +200,4 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default Page;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
import { Task } from '../data/schema';
|
||||
import { labels, priorities, statuses } from '../data/data';
|
||||
import { DataTableToolbar } from './DataTableToolbar';
|
||||
|
@ -15,50 +13,48 @@ export const DataTable: React.FC<DataTableProps> = ({ data }) => {
|
|||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
|
||||
const renderItem = ({ item }: { item: Task }) => {
|
||||
const renderItem = (item: Task) => {
|
||||
const label = labels.find(l => l.value === item.label);
|
||||
const status = statuses.find(s => s.value === item.status);
|
||||
const priority = priorities.find(p => p.value === item.priority);
|
||||
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.cell}>{item.id}</Text>
|
||||
<View style={styles.cell}>
|
||||
<Text style={styles.badge}>{label?.label}</Text>
|
||||
<Text>{item.title}</Text>
|
||||
</View>
|
||||
<View style={styles.cell}>
|
||||
|
||||
<Text>{status?.label}</Text>
|
||||
</View>
|
||||
<View style={styles.cell}>
|
||||
|
||||
<Text>{priority?.label}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.cell}>
|
||||
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<tr key={item.id} className="border-b">
|
||||
<td className="p-2">{item.id}</td>
|
||||
<td className="p-2">
|
||||
<span className="bg-gray-100 rounded-full px-2 py-1 text-xs mr-2">
|
||||
{label?.label}
|
||||
</span>
|
||||
{item.title}
|
||||
</td>
|
||||
<td className="p-2">{status?.label}</td>
|
||||
<td className="p-2">{priority?.label}</td>
|
||||
<td className="p-2">
|
||||
<button className="text-blue-500 hover:text-blue-700">Actions</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<div className="flex flex-col h-full">
|
||||
<DataTableToolbar onFilter={setFilteredData} data={data} />
|
||||
<FlatList
|
||||
data={filteredData.slice(page * rowsPerPage, (page + 1) * rowsPerPage)}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
ListHeaderComponent={() => (
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerCell}>ID</Text>
|
||||
<Text style={styles.headerCell}>Title</Text>
|
||||
<Text style={styles.headerCell}>Status</Text>
|
||||
<Text style={styles.headerCell}>Priority</Text>
|
||||
<Text style={styles.headerCell}>Actions</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<div className="overflow-auto flex-grow">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Priority</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredData.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map(renderItem)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataTablePagination
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
|
@ -66,40 +62,6 @@ export const DataTable: React.FC<DataTableProps> = ({ data }) => {
|
|||
setRowsPerPage={setRowsPerPage}
|
||||
totalItems={filteredData.length}
|
||||
/>
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#ccc',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
headerCell: {
|
||||
flex: 1,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: '#eee',
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
marginRight: 5,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
|
||||
interface DataTablePaginationProps {
|
||||
page: number;
|
||||
|
@ -20,38 +18,84 @@ export const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
|||
const totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>{}</Text>
|
||||
<View style={styles.navigation}>
|
||||
<TouchableOpacity onPress={() => setPage(0)} disabled={page === 0}>
|
||||
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setPage(page - 1)} disabled={page === 0}>
|
||||
|
||||
</TouchableOpacity>
|
||||
<Text>{}</Text>
|
||||
<TouchableOpacity onPress={() => setPage(page + 1)} disabled={page === totalPages - 1}>
|
||||
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setPage(totalPages - 1)} disabled={page === totalPages - 1}>
|
||||
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200">
|
||||
<div className="flex-1 flex justify-between sm:hidden">
|
||||
<button
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page === totalPages - 1}
|
||||
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-700">
|
||||
Showing <span className="font-medium">{page * rowsPerPage + 1}</span> to{' '}
|
||||
<span className="font-medium">
|
||||
{Math.min((page + 1) * rowsPerPage, totalItems)}
|
||||
</span>{' '}
|
||||
of <span className="font-medium">{totalItems}</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={rowsPerPage}
|
||||
onChange={(e) => setRowsPerPage(Number(e.target.value))}
|
||||
className="mr-4 border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
>
|
||||
{[5, 10, 25, 50].map((size) => (
|
||||
<option key={size} value={size}>
|
||||
Show {size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<button
|
||||
onClick={() => setPage(0)}
|
||||
disabled={page === 0}
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<span className="sr-only">First</span>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<span className="sr-only">Previous</span>
|
||||
‹
|
||||
</button>
|
||||
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
Page {page + 1} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page === totalPages - 1}
|
||||
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<span className="sr-only">Next</span>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(totalPages - 1)}
|
||||
disabled={page === totalPages - 1}
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<span className="sr-only">Last</span>
|
||||
»
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#ccc',
|
||||
},
|
||||
navigation: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||
|
||||
import { Task } from '../data/schema';
|
||||
import { priorities, statuses } from '../data/data';
|
||||
|
||||
|
@ -29,53 +27,67 @@ export const DataTableToolbar: React.FC<DataTableToolbarProps> = ({ onFilter, da
|
|||
? filter.filter(f => f !== value)
|
||||
: [...filter, value];
|
||||
setFilter(newFilter);
|
||||
applyFilters();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Filter tasks..."
|
||||
value={searchText}
|
||||
onChangeText={(text) => {
|
||||
setSearchText(text);
|
||||
applyFilters();
|
||||
}}
|
||||
/>
|
||||
<View style={styles.filterContainer}>
|
||||
<TouchableOpacity onPress={() => {}} style={styles.filterButton}>
|
||||
|
||||
<Text>Status</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => {}} style={styles.filterButton}>
|
||||
|
||||
<Text>Priority</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<div className="px-6 py-4 bg-white border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 max-w-lg">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter tasks..."
|
||||
className="block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
applyFilters();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4 flex space-x-4">
|
||||
<div className="relative">
|
||||
<button className="inline-flex justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Status
|
||||
</button>
|
||||
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
|
||||
<div className="py-1">
|
||||
{statuses.map((status) => (
|
||||
<label key={status.value} className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={statusFilter.includes(status.value)}
|
||||
onChange={() => toggleFilter(statusFilter, status.value, setStatusFilter)}
|
||||
className="mr-2"
|
||||
/>
|
||||
{status.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button className="inline-flex justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Priority
|
||||
</button>
|
||||
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
|
||||
<div className="py-1">
|
||||
{priorities.map((priority) => (
|
||||
<label key={priority.value} className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={priorityFilter.includes(priority.value)}
|
||||
onChange={() => toggleFilter(priorityFilter, priority.value, setPriorityFilter)}
|
||||
className="mr-2"
|
||||
/>
|
||||
{priority.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 10,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 5,
|
||||
padding: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
filterContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
filterButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#eee',
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
marginRight: 10,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,34 +1,15 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
|
||||
export const UserNav: React.FC = () => {
|
||||
return (
|
||||
<TouchableOpacity style={styles.container}>
|
||||
|
||||
<View style={styles.userInfo}>
|
||||
<Text style={styles.userName}>John Doe</Text>
|
||||
<Text style={styles.userEmail}>john@example.com</Text>
|
||||
</View>
|
||||
|
||||
</TouchableOpacity>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-shrink-0">
|
||||
<img className="h-10 w-10 rounded-full" src="https://via.placeholder.com/40" alt="User avatar" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">John Doe</div>
|
||||
<div className="text-sm text-gray-500">john@example.com</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
userInfo: {
|
||||
marginLeft: 10,
|
||||
flex: 1,
|
||||
},
|
||||
userName: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
userEmail: {
|
||||
color: 'gray',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import React from 'react';
|
||||
import { SafeAreaView, Text, View, StyleSheet } from 'react-native';
|
||||
import { DataTable } from './components/DataTable';
|
||||
import { UserNav } from './components/UserNav';
|
||||
|
||||
// Mock data - replace with actual data fetching logic
|
||||
const tasks = [
|
||||
{
|
||||
id: "TASK-8782",
|
||||
|
@ -26,42 +24,21 @@ const tasks = [
|
|||
label: "bug",
|
||||
priority: "high"
|
||||
},
|
||||
// ... add more tasks here
|
||||
];
|
||||
|
||||
export default function TaskPage() {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.title}>Welcome back!</Text>
|
||||
<Text style={styles.subtitle}>Here's a list of your tasks for this month!</Text>
|
||||
</View>
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Welcome back!</h1>
|
||||
<p className="text-sm text-gray-500">Here's a list of your tasks for this month!</p>
|
||||
</div>
|
||||
<UserNav />
|
||||
</View>
|
||||
<DataTable data={tasks} />
|
||||
</SafeAreaView>
|
||||
</div>
|
||||
<div className="h-[calc(100vh-80px)]">
|
||||
<DataTable data={tasks} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
subtitle: {
|
||||
color: 'gray',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Album } from "../data/albums"
|
||||
|
||||
interface AlbumArtworkProps {
|
||||
|
@ -16,44 +15,22 @@ export function AlbumArtwork({
|
|||
height,
|
||||
}: AlbumArtworkProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity>
|
||||
<Image
|
||||
source={{ uri: album.cover }}
|
||||
style={[
|
||||
styles.image,
|
||||
{
|
||||
width: width,
|
||||
height: height,
|
||||
aspectRatio: aspectRatio === "portrait" ? 3/4 : 1,
|
||||
},
|
||||
]}
|
||||
<div className="mr-4">
|
||||
<div className="overflow-hidden rounded-md">
|
||||
<img
|
||||
src={album.cover}
|
||||
alt={album.name}
|
||||
className={`object-cover transition-all hover:scale-105 ${aspectRatio === "portrait" ? "aspect-[3/4]" : "aspect-square"}`}
|
||||
style={{
|
||||
width: width,
|
||||
height: height,
|
||||
}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.albumName}>{album.name}</Text>
|
||||
<Text style={styles.artistName}>{album.artist}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
</div>
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<h3 className="font-medium leading-none">{album.name}</h3>
|
||||
<p className="text-xs text-gray-500">{album.artist}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginRight: 16,
|
||||
},
|
||||
image: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
textContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
albumName: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
artistName: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,39 +1,13 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
export function Menu() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity style={styles.menuItem}>
|
||||
<Text style={styles.menuText}>Music</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.menuItem}>
|
||||
<Text style={styles.menuText}>File</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.menuItem}>
|
||||
<Text style={styles.menuText}>Edit</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.menuItem}>
|
||||
<Text style={styles.menuText}>View</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.menuItem}>
|
||||
<Text style={styles.menuText}>Account</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
<div className="flex justify-around py-2 bg-gray-100">
|
||||
<button className="px-4 py-2 text-sm font-medium">Music</button>
|
||||
<button className="px-4 py-2 text-sm font-medium">File</button>
|
||||
<button className="px-4 py-2 text-sm font-medium">Edit</button>
|
||||
<button className="px-4 py-2 text-sm font-medium">View</button>
|
||||
<button className="px-4 py-2 text-sm font-medium">Account</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingVertical: 10,
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
menuItem: {
|
||||
padding: 10,
|
||||
},
|
||||
menuText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,47 +1,17 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
export function PodcastEmptyPlaceholder() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>No episodes added</Text>
|
||||
<Text style={styles.description}>You have not added any podcasts. Add one below.</Text>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Add Podcast</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
<div className="flex items-center justify-center h-full p-8">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-bold">No episodes added</h3>
|
||||
<p className="mb-4 text-sm text-gray-500">
|
||||
You have not added any podcasts. Add one below.
|
||||
</p>
|
||||
<button className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Add Podcast
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
content: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
},
|
||||
description: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#007AFF',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Playlist } from "../data/playlists"
|
||||
|
||||
interface SidebarProps {
|
||||
|
@ -8,83 +7,35 @@ interface SidebarProps {
|
|||
|
||||
export function Sidebar({ playlists }: SidebarProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Discover</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Listen Now</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Browse</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Radio</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Library</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Playlists</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Songs</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Made for You</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Artists</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.buttonText}>Albums</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Playlists</Text>
|
||||
<ScrollView style={styles.playlistContainer}>
|
||||
<div className="p-3">
|
||||
<div className="mb-5">
|
||||
<h2 className="text-lg font-bold mb-2">Discover</h2>
|
||||
<div className="space-y-1 ml-2">
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Listen Now</button>
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Browse</button>
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Radio</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<h2 className="text-lg font-bold mb-2">Library</h2>
|
||||
<div className="space-y-1 ml-2">
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Playlists</button>
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Songs</button>
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Made for You</button>
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Artists</button>
|
||||
<button className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">Albums</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">Playlists</h2>
|
||||
<div className="max-h-72 overflow-y-auto">
|
||||
{playlists?.map((playlist, i) => (
|
||||
<TouchableOpacity style={styles.playlistButton}>
|
||||
<Text style={styles.playlistButtonText}>{playlist}</Text>
|
||||
</TouchableOpacity>
|
||||
<button key={i} className="block w-full text-left px-2 py-1 text-sm hover:bg-gray-100 rounded">
|
||||
{playlist}
|
||||
</button>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 12,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
},
|
||||
buttonContainer: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
playlistContainer: {
|
||||
maxHeight: 300,
|
||||
},
|
||||
playlistButton: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
playlistButtonText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { View, Text, ScrollView, StyleSheet, SafeAreaView } from 'react-native';
|
||||
import { Menu } from './components/menu';
|
||||
import { Sidebar } from './components/sidebar';
|
||||
import { PodcastEmptyPlaceholder } from './components/podcast-empty-placeholder';
|
||||
|
@ -9,72 +8,45 @@ import { playlists } from './data/playlists';
|
|||
|
||||
export default function TemplatesPage() {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<div className="min-h-screen bg-white">
|
||||
<Menu />
|
||||
<View style={styles.content}>
|
||||
<Sidebar playlists={playlists} />
|
||||
<ScrollView style={styles.mainContent}>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Listen Now</Text>
|
||||
<Text style={styles.sectionSubtitle}>Top picks for you. Updated daily.</Text>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.albumList}>
|
||||
{listenNowAlbums.map((album) => (
|
||||
<AlbumArtwork
|
||||
key={album.name}
|
||||
album={album}
|
||||
width={250}
|
||||
height={330}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Made for You</Text>
|
||||
<Text style={styles.sectionSubtitle}>Your personal playlists. Updated daily.</Text>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.albumList}>
|
||||
{madeForYouAlbums.map((album) => (
|
||||
<AlbumArtwork
|
||||
key={album.name}
|
||||
album={album}
|
||||
width={150}
|
||||
height={150}
|
||||
aspectRatio="square"
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
<div className="flex">
|
||||
<div className="w-64 border-r border-gray-200">
|
||||
<Sidebar playlists={playlists} />
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold">Listen Now</h2>
|
||||
<p className="text-gray-500">Top picks for you. Updated daily.</p>
|
||||
<div className="flex overflow-x-auto py-4 space-x-4">
|
||||
{listenNowAlbums.map((album) => (
|
||||
<AlbumArtwork
|
||||
key={album.name}
|
||||
album={album}
|
||||
width={250}
|
||||
height={330}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold">Made for You</h2>
|
||||
<p className="text-gray-500">Your personal playlists. Updated daily.</p>
|
||||
<div className="flex overflow-x-auto py-4 space-x-4">
|
||||
{madeForYouAlbums.map((album) => (
|
||||
<AlbumArtwork
|
||||
key={album.name}
|
||||
album={album}
|
||||
width={150}
|
||||
height={150}
|
||||
aspectRatio="square"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<PodcastEmptyPlaceholder />
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
},
|
||||
albumList: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue