This commit is contained in:
despiegk 2024-10-31 07:24:12 +01:00
parent 676e2d2c0e
commit 8d9018722e
82 changed files with 18553 additions and 0 deletions

View File

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BlocPower - Clean Energy Solutions</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<style>
:root {
--primary: #00b894;
}
.hero {
text-align: center;
padding: 4rem 1rem;
background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
url('https://images.unsplash.com/photo-1613665813446-82a78c468a1d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1200&q=80');
background-size: cover;
background-position: center;
margin: -1rem -1rem 2rem -1rem;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
margin: 2rem 0;
}
.metric {
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.metric h3 {
color: var(--primary);
font-size: 2.5rem;
margin: 0;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-member {
text-align: center;
}
.team-member img {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1rem;
}
.impact-section {
background: rgba(255, 255, 255, 0.05);
padding: 2rem;
border-radius: 8px;
margin: 2rem 0;
}
.tag {
display: inline-block;
padding: 0.2rem 1rem;
background: var(--primary);
border-radius: 20px;
margin: 0.2rem;
font-size: 0.9rem;
}
</style>
</head>
<body>
<main class="container">
<section class="hero">
<h1>BlocPower</h1>
<p class="tagline">Transforming America's Buildings for a Greener Future</p>
</section>
<div class="metrics">
<div class="metric">
<h3>1000+</h3>
<p>Buildings Retrofitted</p>
</div>
<div class="metric">
<h3>40%</h3>
<p>Average Energy Savings</p>
</div>
<div class="metric">
<h3>$50M+</h3>
<p>Capital Deployed</p>
</div>
</div>
<section class="impact-section">
<h2>Our Impact</h2>
<p>BlocPower is transforming America's aging buildings into greener, smarter, healthier facilities. Focusing on underserved communities, we're making clean energy accessible while creating jobs and reducing carbon emissions.</p>
<div class="tags">
<span class="tag">Clean Energy</span>
<span class="tag">Social Impact</span>
<span class="tag">Climate Tech</span>
<span class="tag">Urban Development</span>
</div>
</section>
<section>
<h2>What We Do</h2>
<div class="grid">
<div>
<h3>Building Modernization</h3>
<p>We retrofit aging buildings with modern, clean energy technology, reducing both costs and carbon footprint.</p>
</div>
<div>
<h3>Community Impact</h3>
<p>Creating green jobs and healthier living environments in underserved communities across America.</p>
</div>
</div>
</section>
<section>
<h2>Leadership Team</h2>
<div class="team-grid">
<article class="team-member">
<img src="https://images.unsplash.com/photo-1560250097-0b93528c311a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80" alt="CEO">
<h3>Donnel Baird</h3>
<p>CEO & Founder</p>
</article>
<article class="team-member">
<img src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80" alt="COO">
<h3>Keith Kinch</h3>
<p>General Manager & Co-Founder</p>
</article>
</div>
</section>
<section class="impact-section">
<h2>Investment Highlights</h2>
<ul>
<li>Series B funding led by Microsoft Climate Innovation Fund</li>
<li>Partnership with Goldman Sachs Urban Investment Group</li>
<li>Support from Kapor Capital, Andreessen Horowitz, and other leading investors</li>
</ul>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,301 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BlocPower - Clean Energy Solutions</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<style>
:root {
--primary: #00b894;
}
.hero {
text-align: center;
padding: 4rem 1rem;
background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
url('https://images.unsplash.com/photo-1613665813446-82a78c468a1d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1200&q=80');
background-size: cover;
background-position: center;
margin: -1rem -1rem 2rem -1rem;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
margin: 2rem 0;
}
.metric {
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.metric h3 {
color: var(--primary);
font-size: 2.5rem;
margin: 0;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-member {
text-align: center;
}
.team-member img {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1rem;
}
.impact-section {
background: rgba(255, 255, 255, 0.05);
padding: 2rem;
border-radius: 8px;
margin: 2rem 0;
}
.tag {
display: inline-block;
padding: 0.2rem 1rem;
background: var(--primary);
border-radius: 20px;
margin: 0.2rem;
font-size: 0.9rem;
}
#impact-map {
height: 500px;
width: 100%;
border-radius: 8px;
margin: 2rem 0;
}
/* Custom styles for Leaflet map in dark theme */
.leaflet-tile-pane {
filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%);
}
.location-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.location-card {
background: rgba(255, 255, 255, 0.1);
padding: 1rem;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.location-card:hover {
background: rgba(255, 255, 255, 0.15);
}
.location-card h4 {
margin: 0;
color: var(--primary);
}
/* Hide leaflet attribution */
.leaflet-control-attribution {
display: none;
}
/* Style zoom controls */
.leaflet-control-zoom a {
background-color: rgba(0, 0, 0, 0.7) !important;
color: white !important;
border: 1px solid rgba(255, 255, 255, 0.4) !important;
}
.leaflet-control-zoom a:hover {
background-color: rgba(0, 0, 0, 0.9) !important;
}
.leaflet-control-zoom-in {
border-bottom: 1px solid rgba(255, 255, 255, 0.4) !important;
}
</style>
</head>
<body>
<main class="container">
<section class="hero">
<h1>BlocPower</h1>
<p class="tagline">Transforming America's Buildings for a Greener Future</p>
</section>
<div class="metrics">
<div class="metric">
<h3>1000+</h3>
<p>Buildings Retrofitted</p>
</div>
<div class="metric">
<h3>40%</h3>
<p>Average Energy Savings</p>
</div>
<div class="metric">
<h3>$50M+</h3>
<p>Capital Deployed</p>
</div>
</div>
<section class="impact-section">
<h2>Our Impact</h2>
<p>BlocPower is transforming America's aging buildings into greener, smarter, healthier facilities. Focusing on underserved communities, we're making clean energy accessible while creating jobs and reducing carbon emissions.</p>
<div class="tags">
<span class="tag">Clean Energy</span>
<span class="tag">Social Impact</span>
<span class="tag">Climate Tech</span>
<span class="tag">Urban Development</span>
</div>
</section>
<section>
<h2>Impact Locations</h2>
<div id="impact-map"></div>
<div class="location-info">
<div class="location-card" onclick="focusLocation('nyc')">
<h4>New York City</h4>
<p>Over 500 buildings retrofitted, creating 1000+ green jobs</p>
</div>
<div class="location-card" onclick="focusLocation('chicago')">
<h4>Chicago</h4>
<p>200+ community buildings upgraded with clean energy solutions</p>
</div>
<div class="location-card" onclick="focusLocation('oakland')">
<h4>Oakland</h4>
<p>Partnership with city government for municipal building upgrades</p>
</div>
<div class="location-card" onclick="focusLocation('somewhere')">
<h4>Seomwhere</h4>
<p>Partnership with city government for municipal building upgrades</p>
</div>
</div>
</section>
<section>
<h2>What We Do</h2>
<div class="grid">
<div>
<h3>Building Modernization</h3>
<p>We retrofit aging buildings with modern, clean energy technology, reducing both costs and carbon footprint.</p>
</div>
<div>
<h3>Community Impact</h3>
<p>Creating green jobs and healthier living environments in underserved communities across America.</p>
</div>
</div>
</section>
<section>
<h2>Leadership Team</h2>
<div class="team-grid">
<article class="team-member">
<img src="https://images.unsplash.com/photo-1560250097-0b93528c311a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80" alt="CEO">
<h3>Donnel Baird</h3>
<p>CEO & Founder</p>
</article>
<article class="team-member">
<img src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80" alt="COO">
<h3>Keith Kinch</h3>
<p>General Manager & Co-Founder</p>
</article>
</div>
</section>
<section class="impact-section">
<h2>Investment Highlights</h2>
<ul>
<li>Series B funding led by Microsoft Climate Innovation Fund</li>
<li>Partnership with Goldman Sachs Urban Investment Group</li>
<li>Support from Kapor Capital, Andreessen Horowitz, and other leading investors</li>
</ul>
</section>
</main>
<script>
// Initialize the map
const map = L.map('impact-map').setView([3.8283, -14.5795], 4);
// Add the OpenStreetMap tiles with dark style
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: false
}).addTo(map);
// Create custom white icon
const whiteIcon = L.divIcon({
html: '<div style="width: 10px; height: 10px; background-color: white; border-radius: 50%;"></div>',
className: 'custom-div-icon',
iconSize: [10, 10],
iconAnchor: [5, 5]
});
// Define location data
const locations = {
nyc: {
coords: [40.7128, -74.0060],
title: "New York City",
description: "500+ buildings retrofitted"
},
chicago: {
coords: [41.8781, -87.6298],
title: "Chicago",
description: "200+ community buildings"
},
oakland: {
coords: [37.8044, -122.2711],
title: "Oakland",
description: "Municipal building upgrades"
},
somewhere: {
coords: [3.8044, -14.2711],
title: "Somewhere",
description: "Municipal building upgrades"
}
};
// Add markers for each location
for (const [key, location] of Object.entries(locations)) {
L.marker(location.coords, {icon: whiteIcon})
.bindPopup(`<strong>${location.title}</strong><br>${location.description}`)
.addTo(map);
}
// Function to focus on a specific location
function focusLocation(locationKey) {
const location = locations[locationKey];
map.setView(location.coords, 10);
// Find and open the popup for this location
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
const latLng = layer.getLatLng();
if (latLng.lat === location.coords[0] && latLng.lng === location.coords[1]) {
layer.openPopup();
}
}
});
}
</script>
</body>
</html>

51
examples/images/data.json Normal file
View File

@ -0,0 +1,51 @@
{
"images": [
{
"id": 1,
"url": "https://images.unsplash.com/photo-1728998887922-596106e38ac7?q=80&w=6000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 1",
"title": "Urban Landscape",
"description": "A stunning view of modern architecture and city life captured in perfect lighting conditions"
},
{
"id": 2,
"url": "https://images.unsplash.com/photo-1729614499383-756f6e0e4d80?q=80&w=4332&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 2",
"title": "Natural Wonder",
"description": "An breathtaking natural landscape showcasing the beauty of untouched wilderness"
},
{
"id": 3,
"url": "https://images.unsplash.com/photo-1729505622656-6da75375c3a2?q=80&w=5970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 3",
"title": "Serene Moment",
"description": "A peaceful composition capturing the essence of tranquility and harmony"
},
{
"id": 4,
"url": "https://images.unsplash.com/photo-1729457046342-d444ee10749b?q=80&w=4255&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 4",
"description": "A contemporary view highlighting the intersection of design and functionality"
},
{
"id": 5,
"url": "https://images.unsplash.com/photo-1729476266027-a687e42116ce?q=80&w=5970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 5",
"title": "Abstract Beauty"
},
{
"id": 6,
"url": "https://images.unsplash.com/photo-1729481354989-51e675ddb3d4?q=80&w=3140&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 6",
"title": "Artistic Vision",
"description": "A captivating artistic perspective that challenges conventional viewpoints"
},
{
"id": 7,
"url": "https://images.unsplash.com/photo-1729366791089-6c9643dee806?q=80&w=6000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"alt": "Image 7",
"title": "Dynamic Composition",
"description": "An engaging visual narrative that draws the viewer into its dynamic elements"
}
]
}

112
examples/images/home.css Normal file
View File

@ -0,0 +1,112 @@
:root {
--spacing: 0;
}
.hero {
height: 100vh;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.hero img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
filter: brightness(0.6);
opacity: 1;
transition: opacity 1s ease;
}
.hero img.fade-out {
opacity: 0;
}
.hero img.zooming {
animation: slowZoom 10s forwards;
}
@keyframes slowZoom {
0% {
transform: scale(1);
}
100% {
transform: scale(1.1);
}
}
.hero-content {
text-align: center;
color: white;
padding: 2rem;
max-width: 800px;
}
.hero h1 {
font-size: 4rem;
margin-bottom: 1rem;
}
.lead {
font-size: 1rem;
line-height: 1.3;
font-weight: 200;
margin-top: 1.5rem;
}
.nav-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
max-width: 1200px;
margin: 0 auto;
font-size: 0.9rem;
}
.logo {
font-size: 1.2rem;
font-weight: bold;
color: white;
text-decoration: none;
}
.nav-links {
display: flex;
gap: 1rem;
}
.nav-item {
position: relative;
color: white;
text-decoration: none;
padding: 0.5rem 0;
cursor: pointer;
}
.submenu {
position: absolute;
top: 100%;
left: 0;
background: rgba(0, 0, 0, 0.9);
min-width: 200px;
padding: 0.5rem 0;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.submenu.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.submenu a {
display: block;
padding: 0.5rem 1rem;
color: white;
text-decoration: none;
}
.submenu a:hover {
background: rgba(255, 255, 255, 0.1);
}

View File

@ -0,0 +1,7 @@
<section class="hero">
<img :src="images[currentImageIndex]?.url" :alt="images[currentImageIndex]?.alt">
<div class="hero-content">
<h1>We Are <br>The Crazy Ones</h1>
<p class="lead">The ones called crazy are the only ones truly sane in a world gone mad...</p>
</div>
</section>

100
examples/images/index.html Normal file
View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OurWorld</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css">
<link rel="stylesheet" href="home.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/unpoly@3.9.2/unpoly.min.css">
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/unpoly@3.9.2/unpoly.min.js"></script>
</head>
<body x-data="{
images: [],
currentImageIndex: null,
menuStates: {
about: false,
work: false,
contact: false
},
async init() {
const response = await fetch('data.json');
const data = await response.json();
this.images = data.images;
this.currentImageIndex = Math.floor(Math.random() * this.images.length);
this.$nextTick(() => {
this.startImageAnimation();
});
},
getRandomIndex() {
let newIndex;
do {
newIndex = Math.floor(Math.random() * this.images.length);
} while (newIndex === this.currentImageIndex && this.images.length > 1);
return newIndex;
},
startImageAnimation() {
const img = document.querySelector('.hero img');
img.classList.add('zooming');
img.addEventListener('animationend', () => {
img.classList.add('fade-out');
setTimeout(() => {
this.currentImageIndex = this.getRandomIndex();
img.classList.remove('zooming', 'fade-out');
setTimeout(() => {
img.classList.add('zooming');
}, 50);
}, 1000);
});
},
toggleSubmenu(menu) {
this.menuStates[menu] = !this.menuStates[menu];
Object.keys(this.menuStates).forEach(key => {
if (key !== menu) this.menuStates[key] = false;
});
}
}">
<div class="nav-container">
<nav>
<a href="#" class="logo">Project Mycelium</a>
<div class="nav-links">
<div class="nav-item" @click="toggleSubmenu('about')" @click.away="menuStates.about = false">
About
<div class="submenu" :class="{ 'active': menuStates.about }">
<a href="#our-story">Our Story</a>
<a href="#mission">Mission</a>
<a href="#team">Team</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('work')" @click.away="menuStates.work = false">
Projects
<div class="submenu" :class="{ 'active': menuStates.work }">
<a href="#projects">Projects</a>
<a href="#case-studies">Case Studies</a>
<a href="#impact">Impact</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('contact')" @click.away="menuStates.contact = false">
Contact
<div class="submenu" :class="{ 'active': menuStates.contact }">
<a href="#get-in-touch">Get in Touch</a>
<a href="#locations">Locations</a>
<a href="#careers">Join us.</a>
</div>
</div>
</div>
</nav>
</div>
<section class="hero">
<img :src="images[currentImageIndex]?.url" :alt="images[currentImageIndex]?.alt">
<div class="hero-content">
<h1>We Are <br>The Unconventional Ones</h1>
<p class="lead">The ones called crazy are the only ones truly sane in a world gone mad...</p>
</div>
</section>
</body>
</html>

291
examples/images/index2.html Normal file
View File

@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Grid</title>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
body, html {
margin: 0;
padding: 0;
min-height: 100vh;
}
.container {
max-width: 100% !important;
padding: 0.5rem !important;
margin: 0 !important;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.grid-item {
position: relative;
cursor: pointer;
overflow: hidden;
border-radius: 4px;
}
.grid img {
width: 100%;
height: calc(33.33vh - 1rem);
object-fit: cover;
margin: 0;
transition: transform 0.3s ease;
}
.grid-item:hover img {
transform: scale(1.05);
}
/* Modified image overlay styles */
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 1rem;
text-align: center;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.image-overlay h3 {
margin: 0;
font-size: 1rem;
color: white;
margin-bottom: 0.5rem;
}
.image-overlay p {
margin: 0;
font-size: 0.8rem;
color: white;
line-height: 1.2;
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal.active {
opacity: 1;
visibility: visible;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
.modal img {
max-width: 90%;
max-height: 90vh;
object-fit: contain;
animation: zoomIn 0.3s ease;
}
.close-btn {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
z-index: 1002;
transition: transform 0.3s ease;
}
.close-btn:hover {
transform: scale(1.1);
}
/* Enhanced navigation buttons */
.nav-button {
position: fixed;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.1);
border: none;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
border-radius: 50%;
z-index: 1002;
transition: all 0.3s ease;
}
.nav-button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.1);
}
.nav-button.prev {
left: 30px;
}
.nav-button.next {
right: 30px;
}
.nav-button i {
font-size: 28px;
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.3);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.grid img {
height: calc(25vh - 1rem);
}
.nav-button.prev { left: 10px; }
.nav-button.next { right: 10px; }
.nav-button {
width: 50px;
height: 50px;
}
}
@media (max-width: 480px) {
.grid {
grid-template-columns: 1fr;
}
.grid img {
height: calc(20vh - 1rem);
}
}
</style>
</head>
<body>
<main class="container" x-data="imageGallery()">
<div class="grid">
<template x-for="image in displayedImages" :key="image.uniqueId">
<div class="grid-item" @click="openModal(image)">
<img :src="image.url" :alt="image.alt">
<template x-if="image.title && image.description">
<div class="image-overlay">
<h3 x-text="image.title"></h3>
<p x-text="image.description"></p>
</div>
</template>
</div>
</template>
</div>
<!-- Modal -->
<div id="imageModal"
class="modal"
:class="{ 'active': modalOpen }"
@click.self="closeModal()"
@keydown.window.escape="closeModal()"
@keydown.window.arrow-right="nextImage()"
@keydown.window.arrow-left="previousImage()">
<span class="close-btn" @click="closeModal()">&times;</span>
<div class="modal-content">
<template x-if="currentImage">
<img :src="currentImage.url"
:alt="currentImage.alt"
style="transition: opacity 0.3s ease"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100">
</template>
</div>
<button class="nav-button prev" @click.stop="previousImage">
<i class="bi bi-chevron-left"></i>
</button>
<button class="nav-button next" @click.stop="nextImage">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</main>
<script>
function imageGallery() {
return {
images: [],
displayedImages: [],
modalOpen: false,
currentImage: null,
currentIndex: 0,
async init() {
try {
const response = await fetch('data.json');
const data = await response.json();
this.images = data.images;
// Create array of 12 random images
let tempImages = [];
while (tempImages.length < 12) {
const randomImage = this.images[Math.floor(Math.random() * this.images.length)];
// Create a new object with unique ID to avoid duplicate keys
tempImages.push({
...randomImage,
uniqueId: randomImage.id + '_' + tempImages.length
});
}
this.displayedImages = tempImages;
} catch (error) {
console.error('Error loading images:', error);
}
},
openModal(image) {
this.modalOpen = true;
this.currentImage = image;
this.currentIndex = this.displayedImages.findIndex(img => img.uniqueId === image.uniqueId);
document.body.style.overflow = 'hidden';
},
closeModal() {
this.modalOpen = false;
document.body.style.overflow = 'auto';
},
nextImage() {
if (!this.modalOpen) return;
this.currentIndex = (this.currentIndex + 1) % this.displayedImages.length;
this.currentImage = this.displayedImages[this.currentIndex];
},
previousImage() {
if (!this.modalOpen) return;
this.currentIndex = (this.currentIndex - 1 + this.displayedImages.length) % this.displayedImages.length;
this.currentImage = this.displayedImages[this.currentIndex];
}
}
}
</script>
</body>
</html>

296
examples/images/index3.html Normal file
View File

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Grid</title>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
body, html {
margin: 0;
padding: 0;
min-height: 100vh;
}
.container {
max-width: 100% !important;
padding: 0.5rem !important;
margin: 0 !important;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.grid-item {
position: relative;
cursor: pointer;
overflow: hidden;
border-radius: 4px;
}
.grid img {
width: 100%;
height: calc(33.33vh - 1rem);
object-fit: cover;
margin: 0;
transition: transform 0.3s ease;
}
.grid-item:hover img {
transform: scale(1.05);
}
/* Image overlay styles */
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
padding: 1rem;
text-align: center;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.grid-item:hover .image-overlay {
transform: translateY(0);
}
.image-overlay h3 {
margin: 0;
font-size: 1rem;
color: white;
margin-bottom: 0.5rem;
}
.image-overlay p {
margin: 0;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.8);
line-height: 1.2;
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal.active {
opacity: 1;
visibility: visible;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
.modal img {
max-width: 90%;
max-height: 90vh;
object-fit: contain;
animation: zoomIn 0.3s ease;
}
.close-btn {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
z-index: 1002;
transition: transform 0.3s ease;
}
.close-btn:hover {
transform: scale(1.1);
}
/* Enhanced navigation buttons */
.nav-button {
position: fixed;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.1);
border: none;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
border-radius: 50%;
z-index: 1002;
transition: all 0.3s ease;
}
.nav-button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.1);
}
.nav-button.prev {
left: 30px;
}
.nav-button.next {
right: 30px;
}
.nav-button i {
font-size: 28px;
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.3);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.grid img {
height: calc(25vh - 1rem);
}
.nav-button.prev { left: 10px; }
.nav-button.next { right: 10px; }
.nav-button {
width: 50px;
height: 50px;
}
}
@media (max-width: 480px) {
.grid {
grid-template-columns: 1fr;
}
.grid img {
height: calc(20vh - 1rem);
}
}
</style>
</head>
<body>
<main class="container" x-data="imageGallery()">
<div class="grid">
<template x-for="image in displayedImages" :key="image.uniqueId">
<div class="grid-item" @click="openModal(image)">
<img :src="image.url" :alt="image.alt">
<template x-if="image.title && image.description">
<div class="image-overlay">
<h3 x-text="image.title"></h3>
<p x-text="image.description"></p>
</div>
</template>
</div>
</template>
</div>
<!-- Modal -->
<div id="imageModal"
class="modal"
:class="{ 'active': modalOpen }"
@click.self="closeModal()"
@keydown.window.escape="closeModal()"
@keydown.window.arrow-right="nextImage()"
@keydown.window.arrow-left="previousImage()">
<span class="close-btn" @click="closeModal()">&times;</span>
<div class="modal-content">
<template x-if="currentImage">
<img :src="currentImage.url"
:alt="currentImage.alt"
style="transition: opacity 0.3s ease"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100">
</template>
</div>
<button class="nav-button prev" @click.stop="previousImage">
<i class="bi bi-chevron-left"></i>
</button>
<button class="nav-button next" @click.stop="nextImage">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</main>
<script>
function imageGallery() {
return {
images: [],
displayedImages: [],
modalOpen: false,
currentImage: null,
currentIndex: 0,
async init() {
try {
const response = await fetch('data.json');
const data = await response.json();
this.images = data.images;
// Create array of 12 random images
let tempImages = [];
while (tempImages.length < 12) {
const randomImage = this.images[Math.floor(Math.random() * this.images.length)];
// Create a new object with unique ID to avoid duplicate keys
tempImages.push({
...randomImage,
uniqueId: randomImage.id + '_' + tempImages.length
});
}
this.displayedImages = tempImages;
} catch (error) {
console.error('Error loading images:', error);
}
},
openModal(image) {
this.modalOpen = true;
this.currentImage = image;
this.currentIndex = this.displayedImages.findIndex(img => img.uniqueId === image.uniqueId);
document.body.style.overflow = 'hidden';
},
closeModal() {
this.modalOpen = false;
document.body.style.overflow = 'auto';
},
nextImage() {
if (!this.modalOpen) return;
this.currentIndex = (this.currentIndex + 1) % this.displayedImages.length;
this.currentImage = this.displayedImages[this.currentIndex];
},
previousImage() {
if (!this.modalOpen) return;
this.currentIndex = (this.currentIndex - 1 + this.displayedImages.length) % this.displayedImages.length;
this.currentImage = this.displayedImages[this.currentIndex];
}
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OurWorld</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css">
<link rel="stylesheet" href="home.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/unpoly@3.9.2/unpoly.min.css">
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/unpoly@3.9.2/unpoly.min.js"></script>
</head>
<body x-data="{
images: [],
currentImageIndex: null,
menuStates: {
about: false,
work: false,
contact: false
},
async init() {
const response = await fetch('data.json');
const data = await response.json();
this.images = data.images;
this.currentImageIndex = Math.floor(Math.random() * this.images.length);
this.$nextTick(() => {
this.startImageAnimation();
});
},
getRandomIndex() {
let newIndex;
do {
newIndex = Math.floor(Math.random() * this.images.length);
} while (newIndex === this.currentImageIndex && this.images.length > 1);
return newIndex;
},
startImageAnimation() {
const img = document.querySelector('.hero img');
img.classList.add('zooming');
img.addEventListener('animationend', () => {
img.classList.add('fade-out');
setTimeout(() => {
this.currentImageIndex = this.getRandomIndex();
img.classList.remove('zooming', 'fade-out');
setTimeout(() => {
img.classList.add('zooming');
}, 50);
}, 1000);
});
},
toggleSubmenu(menu) {
this.menuStates[menu] = !this.menuStates[menu];
Object.keys(this.menuStates).forEach(key => {
if (key !== menu) this.menuStates[key] = false;
});
}
}">
<div class="nav-container">
<nav>
<a href="#" class="logo">OURWORLD</a>
<div class="nav-links">
<div class="nav-item" @click="toggleSubmenu('about')" @click.away="menuStates.about = false">
About
<div class="submenu" :class="{ 'active': menuStates.about }">
<a href="#our-story">Our Story</a>
<a href="#mission">Mission</a>
<a href="#team">Team</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('work')" @click.away="menuStates.work = false">
Projects
<div class="submenu" :class="{ 'active': menuStates.work }">
<a href="#projects">Projects</a>
<a href="#case-studies">Case Studies</a>
<a href="#impact">Impact</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('contact')" @click.away="menuStates.contact = false">
Contact
<div class="submenu" :class="{ 'active': menuStates.contact }">
<a href="#get-in-touch">Get in Touch</a>
<a href="#locations">Locations</a>
<a href="#careers">Join us.</a>
</div>
</div>
</div>
</nav>
</div>
<section class="hero">
<img :src="images[currentImageIndex]?.url" :alt="images[currentImageIndex]?.alt">
<div class="hero-content">
<h1>We Are <br>The Crazy Ones</h1>
<p class="lead">The ones called crazy are the only ones truly sane in a world gone mad...</p>
</div>
</section>
</body>
</html>

View File

@ -0,0 +1,340 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Grid</title>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
body, html {
margin: 0;
padding: 0;
min-height: 100vh;
}
.container {
max-width: 100% !important;
padding: 0.5rem !important;
margin: 0 !important;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.grid-item {
position: relative;
cursor: pointer;
overflow: hidden;
border-radius: 4px;
}
.grid img {
width: 100%;
height: calc(33.33vh - 1rem);
object-fit: cover;
margin: 0;
}
/* Multiple zoom animations with different timings */
@keyframes slowZoom1 {
0% { transform: scale(1); }
45% { transform: scale(1.1); }
55% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes slowZoom2 {
0% { transform: scale(1.05); }
50% { transform: scale(1); }
100% { transform: scale(1.05); }
}
@keyframes slowZoom3 {
0% { transform: scale(1); }
30% { transform: scale(1.08); }
70% { transform: scale(1.08); }
100% { transform: scale(1); }
}
@keyframes slowZoom4 {
0% { transform: scale(1.02); }
40% { transform: scale(1.1); }
60% { transform: scale(1.1); }
100% { transform: scale(1.02); }
}
/* Apply different animations to different images */
.grid-item:nth-child(4n+1) img {
animation: slowZoom1 20s infinite ease-in-out;
animation-delay: -5s;
}
.grid-item:nth-child(4n+2) img {
animation: slowZoom2 20s infinite ease-in-out;
animation-delay: -10s;
}
.grid-item:nth-child(4n+3) img {
animation: slowZoom3 20s infinite ease-in-out;
animation-delay: -15s;
}
.grid-item:nth-child(4n+4) img {
animation: slowZoom4 20s infinite ease-in-out;
animation-delay: -2s;
}
.grid-item:hover img {
animation: none;
transform: scale(1.05);
}
/* Image overlay styles */
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
padding: 1rem;
text-align: center;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.grid-item:hover .image-overlay {
transform: translateY(0);
}
.image-overlay h3 {
margin: 0;
font-size: 1rem;
color: white;
margin-bottom: 0.5rem;
}
.image-overlay p {
margin: 0;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.8);
line-height: 1.2;
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal.active {
opacity: 1;
visibility: visible;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
.modal img {
max-width: 90%;
max-height: 90vh;
object-fit: contain;
animation: zoomIn 0.3s ease;
}
.close-btn {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
z-index: 1002;
transition: transform 0.3s ease;
}
.close-btn:hover {
transform: scale(1.1);
}
/* Enhanced navigation buttons */
.nav-button {
position: fixed;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.1);
border: none;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
border-radius: 50%;
z-index: 1002;
transition: all 0.3s ease;
}
.nav-button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.1);
}
.nav-button.prev {
left: 30px;
}
.nav-button.next {
right: 30px;
}
.nav-button i {
font-size: 28px;
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.3);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.grid img {
height: calc(25vh - 1rem);
}
.nav-button.prev { left: 10px; }
.nav-button.next { right: 10px; }
.nav-button {
width: 50px;
height: 50px;
}
}
@media (max-width: 480px) {
.grid {
grid-template-columns: 1fr;
}
.grid img {
height: calc(20vh - 1rem);
}
}
</style>
</head>
<body>
<main class="container" x-data="imageGallery()">
<div class="grid">
<template x-for="image in displayedImages" :key="image.uniqueId">
<div class="grid-item" @click="openModal(image)">
<img :src="image.url" :alt="image.alt">
<template x-if="image.title && image.description">
<div class="image-overlay">
<h3 x-text="image.title"></h3>
<p x-text="image.description"></p>
</div>
</template>
</div>
</template>
</div>
<!-- Modal -->
<div id="imageModal"
class="modal"
:class="{ 'active': modalOpen }"
@click.self="closeModal()"
@keydown.window.escape="closeModal()"
@keydown.window.arrow-right="nextImage()"
@keydown.window.arrow-left="previousImage()">
<span class="close-btn" @click="closeModal()">&times;</span>
<div class="modal-content">
<template x-if="currentImage">
<img :src="currentImage.url"
:alt="currentImage.alt"
style="transition: opacity 0.3s ease"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100">
</template>
</div>
<button class="nav-button prev" @click.stop="previousImage">
<i class="bi bi-chevron-left"></i>
</button>
<button class="nav-button next" @click.stop="nextImage">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</main>
<script>
function imageGallery() {
return {
images: [],
displayedImages: [],
modalOpen: false,
currentImage: null,
currentIndex: 0,
async init() {
try {
const response = await fetch('data.json');
const data = await response.json();
this.images = data.images;
// Create array of 12 random images
let tempImages = [];
while (tempImages.length < 12) {
const randomImage = this.images[Math.floor(Math.random() * this.images.length)];
// Create a new object with unique ID to avoid duplicate keys
tempImages.push({
...randomImage,
uniqueId: randomImage.id + '_' + tempImages.length
});
}
this.displayedImages = tempImages;
} catch (error) {
console.error('Error loading images:', error);
}
},
openModal(image) {
this.modalOpen = true;
this.currentImage = image;
this.currentIndex = this.displayedImages.findIndex(img => img.uniqueId === image.uniqueId);
document.body.style.overflow = 'hidden';
},
closeModal() {
this.modalOpen = false;
document.body.style.overflow = 'auto';
},
nextImage() {
if (!this.modalOpen) return;
this.currentIndex = (this.currentIndex + 1) % this.displayedImages.length;
this.currentImage = this.displayedImages[this.currentIndex];
},
previousImage() {
if (!this.modalOpen) return;
this.currentIndex = (this.currentIndex - 1 + this.displayedImages.length) % this.displayedImages.length;
this.currentImage = this.displayedImages[this.currentIndex];
}
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OurWorld</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css">
<link rel="stylesheet" href="styles.css">
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script defer src="script.js"></script>
</head>
<body>
<div x-data="navigation">
<div class="nav-container">
<nav>
<a href="#" class="logo">OURWORLD</a>
<div class="nav-links">
<div class="nav-item" @click="toggleSubmenu('about')" @click.away="menuStates.about = false">
About
<div class="submenu" :class="{ 'active': menuStates.about }">
<a href="#our-story">Our Story</a>
<a href="#mission">Mission</a>
<a href="#team">Team</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('work')" @click.away="menuStates.work = false">
Projects
<div class="submenu" :class="{ 'active': menuStates.work }">
<a href="#projects">Projects</a>
<a href="#case-studies">Case Studies</a>
<a href="#impact">Impact</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('contact')" @click.away="menuStates.contact = false">
Contact
<div class="submenu" :class="{ 'active': menuStates.contact }">
<a href="#get-in-touch">Get in Touch</a>
<a href="#locations">Locations</a>
<a href="#careers">Join us.</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<div x-data="mainApp">
<section class="hero">
<img :src="images[currentImageIndex]?.url" :alt="images[currentImageIndex]?.alt">
<div class="hero-content">
<h1>We Are <br>The Crazy Ones</h1>
<p class="lead">The ones called crazy are the only ones truly sane in a world gone mad...</p>
</div>
</section>
</div>
</body>
</html>

31
examples/images/menu.html Normal file
View File

@ -0,0 +1,31 @@
<div class="nav-container" up-nav>
<nav>
<a href="home.html" class="logo" up-target="[up-fragment='content']">OURWORLD</a>
<div class="nav-links">
<div class="nav-item" @click="toggleSubmenu('about')" @click.away="menuStates.about = false">
About
<div class="submenu" :class="{ 'active': menuStates.about }">
<a href="about/story.html" up-target="[up-fragment='content']">Our Story</a>
<a href="about/mission.html" up-target="[up-fragment='content']">Mission</a>
<a href="about/team.html" up-target="[up-fragment='content']">Team</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('work')" @click.away="menuStates.work = false">
Projects
<div class="submenu" :class="{ 'active': menuStates.work }">
<a href="projects/list.html" up-target="[up-fragment='content']">Projects</a>
<a href="projects/cases.html" up-target="[up-fragment='content']">Case Studies</a>
<a href="projects/impact.html" up-target="[up-fragment='content']">Impact</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('contact')" @click.away="menuStates.contact = false">
Contact
<div class="submenu" :class="{ 'active': menuStates.contact }">
<a href="contact/touch.html" up-target="[up-fragment='content']">Get in Touch</a>
<a href="contact/locations.html" up-target="[up-fragment='content']">Locations</a>
<a href="contact/careers.html" up-target="[up-fragment='content']">Join us.</a>
</div>
</div>
</div>
</nav>
</div>

View File

@ -0,0 +1,33 @@
<template id="navigation-template">
<div class="nav-container">
<nav>
<a href="#" class="logo">OURWORLD</a>
<div class="nav-links">
<div class="nav-item" @click="toggleSubmenu('about')" @click.away="menuStates.about = false">
About
<div class="submenu" :class="{ 'active': menuStates.about }">
<a href="#our-story">Our Story</a>
<a href="#mission">Mission</a>
<a href="#team">Team</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('work')" @click.away="menuStates.work = false">
Projects
<div class="submenu" :class="{ 'active': menuStates.work }">
<a href="#projects">Projects</a>
<a href="#case-studies">Case Studies</a>
<a href="#impact">Impact</a>
</div>
</div>
<div class="nav-item" @click="toggleSubmenu('contact')" @click.away="menuStates.contact = false">
Contact
<div class="submenu" :class="{ 'active': menuStates.contact }">
<a href="#get-in-touch">Get in Touch</a>
<a href="#locations">Locations</a>
<a href="#careers">Join us.</a>
</div>
</div>
</div>
</nav>
</div>
</template>

52
examples/images/script.js Normal file
View File

@ -0,0 +1,52 @@
document.addEventListener('alpine:init', () => {
Alpine.data('navigation', () => ({
menuStates: {
about: false,
work: false,
contact: false
},
toggleSubmenu(menu) {
this.menuStates[menu] = !this.menuStates[menu];
Object.keys(this.menuStates).forEach(key => {
if (key !== menu) this.menuStates[key] = false;
});
}
}));
Alpine.data('mainApp', () => ({
images: [],
currentImageIndex: null,
async init() {
const response = await fetch('data.json');
const data = await response.json();
this.images = data.images;
this.currentImageIndex = Math.floor(Math.random() * this.images.length);
this.$nextTick(() => {
this.startImageAnimation();
});
},
getRandomIndex() {
let newIndex;
do {
newIndex = Math.floor(Math.random() * this.images.length);
} while (newIndex === this.currentImageIndex && this.images.length > 1);
return newIndex;
},
startImageAnimation() {
const img = document.querySelector('.hero img');
img.classList.add('zooming');
img.addEventListener('animationend', () => {
img.classList.add('fade-out');
setTimeout(() => {
this.currentImageIndex = this.getRandomIndex();
img.classList.remove('zooming', 'fade-out');
setTimeout(() => {
img.classList.add('zooming');
}, 50);
}, 1000);
});
}
}));
});

19
examples/images/server.py Normal file
View File

@ -0,0 +1,19 @@
from http.server import HTTPServer, SimpleHTTPRequestHandler
import sys
class CORSRequestHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
super().end_headers()
if __name__ == '__main__':
port = 8000
server_address = ('', port)
httpd = HTTPServer(server_address, CORSRequestHandler)
print(f"Server running on port {port}")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server")
httpd.server_close()
sys.exit(0)

5
examples/images/start.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
python3 -m http.server 8000 &
sleep 1
open http://localhost:8000
wait

94
react-shadcn-starter/.gitignore vendored Normal file
View File

@ -0,0 +1,94 @@
# Dependencies
node_modules/
/.pnp
.pnp.js
.npm
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Testing
/coverage
.nyc_output
*.lcov
# Production & Build
/build
/dist
dist-ssr
*.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*.local
# Cache & Temporary files
.cache/
.temp/
.eslintcache
.stylelintcache
*.tsbuildinfo
.next
.nuxt
.vuepress/dist
.docusaurus
.serverless/
.fusebox/
.dynamodb/
.tern-port
.webpack/
.grunt
# Editor & IDE files
.idea/
.vscode/*
!.vscode/extensions.json
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sublime-workspace
*.sublime-project
.project
.classpath
.settings/
*.iml
*.ipr
*.iws
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Debug
.debug/
debug/
debug.log
# Misc
*.pid
*.seed
*.pid.lock
.lock-wscript

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Hayyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,77 @@
# React Shadcn Starter
React + Vite + TypeScript template for building apps with shadcn/ui.
## Getting Started
```
git clone https://github.com/hayyi2/react-shadcn-starter.git new-project
cd new-project
npm install
npm run dev
```
## Getting Done
- [x] Single page app with navigation and responsif layout
- [x] Customable configuration `/config`
- [x] Simple starting page/feature `/pages`
- [x] Github action deploy github pages
## Deploy `gh-pages`
- change `basenameProd` in `/vite.config.ts`
- create deploy key `GITHUB_TOKEN` in github `/settings/keys`
- commit and push changes code
- setup gihub pages to branch `gh-pages`
- run action `Build & Deploy`
### Auto Deploy
- change file `.github/workflows/build-and-deploy.yml`
- Comment on `workflow_dispatch`
- Uncomment on `push`
```yaml
# on:
# workflow_dispatch:
on:
push:
branches: ["main"]
```
## Features
- React + Vite + TypeScript
- Tailwind CSS
- [react-router-dom](https://www.npmjs.com/package/react-router-dom)
- [shadcn-ui](https://github.com/shadcn-ui/ui/)
- [radix-ui/icons](https://www.radix-ui.com/icons)
## Project Structure
```
react-shadcn-starter/
├── public/ # Public assets
├── src/ # Application source code
│ ├── components/ # React components
│ │ └── ui/ # shadc/ui components
│ │ └── layouts/ # layouts components
│ ├── context/ # contexts components
│ ├── config/ # Config data
│ ├── hook/ # Custom hooks
│ ├── lib/ # Utility functions
│ ├── pages/ # pages/features components
│ ├── App.tsx # Application entry point
│ ├── index.tsx # Main rendering file
│ └── Router.tsx # Routes component
├── index.html # HTML entry point
├── postcss.config.js # PostCSS configuration
├── tailwind.config.js # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
└── vite.config.ts # Vite configuration
```
## License
This project is licensed under the MIT License. See the [LICENSE](https://github.com/hayyi2/react-shadcn-starter/blob/main/LICENSE) file for details.

View File

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

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Shadcn Starter</title>
</head>
<body class="min-h-screen">
<div id="root" class="relative flex min-h-screen flex-col"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

12033
react-shadcn-starter/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
{
"name": "react-shadcn-starter",
"private": true,
"version": "0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.0",
"@codemirror/lang-yaml": "^6.1.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@uiw/react-codemirror": "^4.23.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"lucide-react": "^0.454.0",
"md5": "^2.3.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.16.0",
"react-wrap-balancer": "^1.1.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.2",
"webdav": "^5.7.1"
},
"devDependencies": {
"@types/md5": "^2.3.5",
"@types/node": "^20.17.3",
"@types/react": "^18.0.17",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.1.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^4.6.4",
"vite": "^3.1.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24">
<rect x="2" y="2" width="20" height="20" rx="7" fill="#0F172A"/>
</svg>

After

Width:  |  Height:  |  Size: 99 B

View File

@ -0,0 +1,26 @@
import React from 'react'
import { WebdavFileManager } from './components/webdav-file-manager'
import { Toaster } from './components/ui/toaster'
import { OurCalendar } from './components/calendar'
import { WebDAVConfig } from './lib/calendar-data'
const webdavConfig: WebDAVConfig = {
url: 'https://1824-86-111-139-199.ngrok-free.app',
username: 'admin',
password: '1234',
}
function App() {
return (
<>
<OurCalendar
webdavConfig={webdavConfig}
calendarFile="/calendars/kristof.json"
/>
{/* <WebdavFileManager /> */}
<Toaster />
</>
)
}
export default App

View File

@ -0,0 +1,35 @@
import { createBrowserRouter } from "react-router-dom";
import { Applayout } from "./components/layouts/AppLayout";
import NoMatch from "./pages/NoMatch";
import Dashboard from "./pages/Dashboard";
import Empty from "./pages/Empty";
import Sample from "./pages/Sample";
export const router = createBrowserRouter([
{
path: "/",
element: <Applayout />,
children: [
{
path: "",
element: <Dashboard />,
},
{
path: "sample",
element: <Sample />,
},
{
path: "empty",
element: <Empty />,
},
],
},
{
path: "*",
element: <NoMatch />,
},
], {
basename: global.basename
})

View File

@ -0,0 +1,535 @@
import * as React from 'react'
import { addDays, format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, parseISO } from 'date-fns'
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, MoreHorizontal, Plus, Moon, Sun, Clock } from 'lucide-react'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
import ReactMarkdown from 'react-markdown'
import { v4 as uuidv4 } from 'uuid'
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Calendar } from "@/components/ui/calendar"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useToast } from "@/components/ui/use-toast"
import { Switch } from "@/components/ui/switch"
import {
Event,
categories,
defaultEvents,
CalendarDataManager,
timeOptions,
WebDAVConfig
} from '@/lib/calendar-data'
const dataManager = CalendarDataManager.getInstance();
interface CalendarProps {
webdavConfig: WebDAVConfig;
calendarFile: string;
}
export function OurCalendar({ webdavConfig, calendarFile }: CalendarProps) {
const [currentDate, setCurrentDate] = React.useState(new Date())
const [events, setEvents] = React.useState<Event[]>([])
const [selectedEvent, setSelectedEvent] = React.useState<Event | null>(null)
const [isAddEventOpen, setIsAddEventOpen] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true)
const [error, setError] = React.useState<string | null>(null)
const [isUsingDefaultData, setIsUsingDefaultData] = React.useState(false)
const [isDarkMode, setIsDarkMode] = React.useState(false)
const { toast } = useToast()
React.useEffect(() => {
dataManager.setConfig(webdavConfig, calendarFile);
}, [webdavConfig, calendarFile]);
const startDate = startOfWeek(startOfMonth(currentDate))
const endDate = endOfWeek(endOfMonth(currentDate))
const days = eachDayOfInterval({ start: startDate, end: endDate })
const handlePrevMonth = () => setCurrentDate(addDays(startDate, -7))
const handleNextMonth = () => setCurrentDate(addDays(startDate, 7))
const fetchCalendarData = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${webdavConfig.url}${calendarFile}`, {
headers: {
'Authorization': 'Basic ' + btoa(`${webdavConfig.username}:${webdavConfig.password}`)
}
})
if (response.status === 404) {
await dataManager.saveEvents(defaultEvents)
setEvents(defaultEvents)
setIsUsingDefaultData(false)
toast({
title: "Calendar Initialized",
description: "A new calendar has been created with default events.",
})
} else if (!response.ok) {
throw new Error(`Failed to fetch calendar data: ${response.status} ${response.statusText}`)
} else {
const data = await response.json()
// Ensure all events have the new fields
const updatedEvents = data.events.map((event: Event) => ({
...event,
attendees: event.attendees || [],
isFullDay: event.isFullDay || false
}))
if (dataManager.hasChanged(updatedEvents)) {
setEvents(updatedEvents)
}
setIsUsingDefaultData(false)
}
} catch (err) {
console.error('Error fetching calendar data:', err)
setEvents(defaultEvents)
setIsUsingDefaultData(true)
setError(`Error: ${err instanceof Error ? err.message : 'Unknown error occurred'}. Using default data. Changes will not be saved.`)
} finally {
setTimeout(() => setIsLoading(false), 300)
}
}
const checkForUpdates = async () => {
if (selectedEvent) return; // Skip update check while editing
try {
const fetchedEvents = await dataManager.fetchEvents();
// Ensure all events have the new fields
const updatedEvents = fetchedEvents.map(event => ({
...event,
attendees: event.attendees || [],
isFullDay: event.isFullDay || false
}))
if (dataManager.hasChanged(updatedEvents)) {
setEvents(updatedEvents);
toast({
title: "Calendar Updated",
description: "Your calendar has been updated with the latest changes.",
});
}
} catch (err) {
console.error('Error checking for updates:', err)
}
}
React.useEffect(() => {
fetchCalendarData()
const intervalId = setInterval(checkForUpdates, 5000)
return () => clearInterval(intervalId)
}, [])
React.useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, [isDarkMode])
const saveCalendarData = async (newEvents: Event[]) => {
if (isUsingDefaultData) {
toast({
title: "Changes not saved",
description: "Using default data. Changes will not be persisted.",
variant: "destructive",
})
return
}
try {
await dataManager.saveEvents(newEvents);
toast({
title: "Changes saved",
description: "Your changes have been successfully saved.",
})
} catch (err) {
console.error('Error saving calendar data:', err)
toast({
title: "Error saving changes",
description: "There was a problem saving your changes. Please try again.",
variant: "destructive",
})
}
}
const handleAddEvent = async (event: Event) => {
const newEvents = [...events, event]
setEvents(newEvents)
setIsAddEventOpen(false)
await saveCalendarData(newEvents)
}
const handleEditEvent = async (updatedEvent: Event) => {
setEvents(prev => prev.map(event => event.id === updatedEvent.id ? updatedEvent : event))
setSelectedEvent(null)
await saveCalendarData(events.map(event => event.id === updatedEvent.id ? updatedEvent : event))
}
const handleDeleteEvent = async (id: string) => {
setEvents(prev => prev.filter(event => event.id !== id))
setSelectedEvent(null)
await saveCalendarData(events.filter(event => event.id !== id))
}
const onDragEnd = async (result: any) => {
if (!result.destination) return
const newEvents = Array.from(events)
const [reorderedItem] = newEvents.splice(result.source.index, 1)
newEvents.splice(result.destination.index, 0, {
...reorderedItem,
date: days[result.destination.droppableId].toISOString().split('T')[0],
})
setEvents(newEvents)
await saveCalendarData(newEvents)
}
return (
<div className={`container mx-auto p-4 ${isDarkMode ? 'dark' : ''}`}>
<div className="dark:bg-gray-800 min-h-screen transition-colors duration-300">
<div className={`transition-opacity duration-300 ${isLoading ? 'opacity-0' : 'opacity-100'}`}>
{error && (
<div className="bg-yellow-100 dark:bg-yellow-800 border-l-4 border-yellow-500 text-yellow-700 dark:text-yellow-200 p-4 mb-4" role="alert">
<p className="font-bold">Error</p>
<p>{error}</p>
{isUsingDefaultData && (
<p className="mt-2">
<strong>Note:</strong> Using default data. Changes will not be saved to the server.
</p>
)}
</div>
)}
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold dark:text-white">{format(currentDate, 'MMMM yyyy')}</h1>
<div className="flex items-center space-x-2">
<Button onClick={handlePrevMonth} variant="outline" size="icon" className="dark:bg-gray-700 dark:text-white">
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous month</span>
</Button>
<Button onClick={handleNextMonth} variant="outline" size="icon" className="dark:bg-gray-700 dark:text-white">
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next month</span>
</Button>
<Dialog open={isAddEventOpen} onOpenChange={setIsAddEventOpen}>
<DialogTrigger asChild>
<Button className="dark:bg-blue-600 dark:text-white">
<Plus className="h-4 w-4 mr-2" />
Add Event
</Button>
</DialogTrigger>
<DialogContent className="dark:bg-gray-800 dark:text-white">
<EventForm onSubmit={handleAddEvent} isDarkMode={isDarkMode} />
</DialogContent>
</Dialog>
<div className="flex items-center space-x-2">
<Switch
checked={isDarkMode}
onCheckedChange={setIsDarkMode}
className="dark:bg-gray-600"
/>
<Label htmlFor="dark-mode" className="dark:text-white">
{isDarkMode ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
</Label>
</div>
</div>
</div>
<DragDropContext onDragEnd={onDragEnd}>
<div className="grid grid-cols-7 gap-2">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
<div key={day} className="text-center font-bold p-2 dark:text-white">
{day}
</div>
))}
{days.map((day, index) => (
<Droppable key={day.toISOString()} droppableId={index.toString()}>
{(provided: any) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={cn(
"border rounded-lg p-2 min-h-[100px] dark:border-gray-600",
!isSameMonth(day, currentDate) && "bg-gray-100 dark:bg-gray-700",
isSameMonth(day, currentDate) && "dark:bg-gray-800"
)}
>
<div className="text-right dark:text-white">{format(day, 'd')}</div>
{events
.filter((event) => isSameDay(parseISO(event.date), day))
.map((event, eventIndex) => (
<Draggable key={event.id} draggableId={event.id} index={eventIndex}>
{(provided: any) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={cn(
"mt-1 p-1 text-sm rounded cursor-pointer",
event.color
)}
onClick={() => setSelectedEvent(event)}
>
<div className="flex items-center justify-between">
<span>{event.title}</span>
{!event.isFullDay && (
<span className="text-xs opacity-75">
<Clock className="h-3 w-3 inline-block mr-1" />
{event.time}
</span>
)}
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
))}
</div>
</DragDropContext>
{selectedEvent && (
<EventDetailsDialog
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
onEdit={handleEditEvent}
onDelete={handleDeleteEvent}
isDarkMode={isDarkMode}
/>
)}
</div>
</div>
</div>
)
}
function EventForm({ onSubmit, initialData, isDarkMode }: { onSubmit: (event: Event) => void, initialData?: Event, isDarkMode: boolean }) {
const [title, setTitle] = React.useState(initialData?.title || '')
const [date, setDate] = React.useState<Date | undefined>(initialData ? parseISO(initialData.date) : undefined)
const [time, setTime] = React.useState(initialData?.time || '10:00')
const [duration, setDuration] = React.useState(initialData?.duration?.toString() || '60')
const [category, setCategory] = React.useState(initialData?.category || '')
const [content, setContent] = React.useState(initialData?.content || '')
const [attendees, setAttendees] = React.useState(initialData?.attendees?.join('\n') || '')
const [isFullDay, setIsFullDay] = React.useState(initialData?.isFullDay || false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (title && date && category) {
const color = categories.find(c => c.name === category)?.color || 'bg-gray-500 dark:bg-gray-700'
onSubmit({
id: initialData?.id || uuidv4(),
title,
date: date.toISOString().split('T')[0],
time: isFullDay ? '00:00' : time,
duration: parseInt(duration),
category,
color,
content,
attendees: attendees.split('\n').filter(email => email.trim() !== ''),
isFullDay
})
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="title" className="dark:text-white">Title</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
className="dark:bg-gray-700 dark:text-white dark:border-gray-600"
/>
</div>
<div>
<Label htmlFor="date" className="dark:text-white">Date</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground",
"dark:bg-gray-700 dark:text-white dark:border-gray-600"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
initialFocus
className={isDarkMode ? "dark" : ""}
/>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center space-x-2">
<Switch
id="isFullDay"
checked={isFullDay}
onCheckedChange={setIsFullDay}
className="dark:bg-gray-600"
/>
<Label htmlFor="isFullDay" className="dark:text-white">Full Day Event</Label>
</div>
{!isFullDay && (
<div>
<Label htmlFor="time" className="dark:text-white">Time</Label>
<Select value={time} onValueChange={setTime}>
<SelectTrigger className="dark:bg-gray-700 dark:text-white dark:border-gray-600">
<SelectValue placeholder="Select a time" />
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:text-white max-h-[200px]">
{timeOptions.map((timeOption) => (
<SelectItem key={timeOption} value={timeOption}>
{timeOption}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{!isFullDay && (
<div>
<Label htmlFor="duration" className="dark:text-white">Duration (minutes)</Label>
<Select value={duration} onValueChange={setDuration}>
<SelectTrigger className="dark:bg-gray-700 dark:text-white dark:border-gray-600">
<SelectValue placeholder="Select duration" />
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:text-white">
{[15, 30, 45, 60, 90, 120, 180, 240].map((mins) => (
<SelectItem key={mins} value={mins.toString()}>
{dataManager.formatDuration(mins)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div>
<Label htmlFor="category" className="dark:text-white">Category</Label>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger className="dark:bg-gray-700 dark:text-white dark:border-gray-600">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:text-white">
{categories.map((cat) => (
<SelectItem key={cat.name} value={cat.name}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="attendees" className="dark:text-white">Attendees (one email per line)</Label>
<Textarea
id="attendees"
value={attendees}
onChange={(e) => setAttendees(e.target.value)}
rows={3}
className="dark:bg-gray-700 dark:text-white dark:border-gray-600"
placeholder="john@example.com&#10;jane@example.com"
/>
</div>
<div>
<Label htmlFor="content" className="dark:text-white">Content (Markdown)</Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={5}
className="dark:bg-gray-700 dark:text-white dark:border-gray-600"
/>
</div>
<Button type="submit" className="dark:bg-blue-600 dark:text-white">{initialData ? 'Update' : 'Add'} Event</Button>
</form>
)
}
function EventDetailsDialog({ event, onClose, onEdit, onDelete, isDarkMode }: {
event: Event
onClose: () => void
onEdit: (event: Event) => void
onDelete: (id: string) => void
isDarkMode: boolean
}) {
const [isEditing, setIsEditing] = React.useState(false)
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="dark:bg-gray-800 dark:text-white">
<DialogHeader>
<DialogTitle>{isEditing ? 'Edit Event' : event.title}</DialogTitle>
</DialogHeader>
{isEditing ? (
<EventForm onSubmit={(updatedEvent) => {
onEdit(updatedEvent)
setIsEditing(false)
}} initialData={event} isDarkMode={isDarkMode} />
) : (
<>
<div className="space-y-2">
<p><strong>Date:</strong> {format(parseISO(event.date), 'PPP')}</p>
{!event.isFullDay && (
<>
<p><strong>Time:</strong> {event.time}</p>
<p><strong>Duration:</strong> {dataManager.formatDuration(event.duration)}</p>
</>
)}
<p><strong>Category:</strong> <span className={`px-2 py-1 rounded ${event.color} text-white`}>{event.category}</span></p>
{(event.attendees || []).length > 0 && (
<div>
<strong>Attendees:</strong>
<ul className="list-disc list-inside">
{(event.attendees || []).map((email, index) => (
<li key={index}>{email}</li>
))}
</ul>
</div>
)}
<div className="prose max-w-none dark:prose-invert">
<ReactMarkdown>{event.content}</ReactMarkdown>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditing(true)} className="dark:bg-gray-700 dark:text-white">Edit</Button>
<Button variant="destructive" onClick={() => onDelete(event.id)} className="dark:bg-red-600 dark:text-white">Delete</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}

View File

@ -0,0 +1,17 @@
type IconProps = React.HTMLAttributes<SVGElement>
export const Icons = {
logo: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<rect x="2" y="2" width="20" height="20" rx="7" fill="#0F172A"/>
</svg>
),
gitHub: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
}

View File

@ -0,0 +1,19 @@
import { Outlet } from "react-router-dom";
import { Header } from "./Header";
import { Footer } from "./Footer";
export function Applayout() {
return (
<>
<Header />
<div className="flex-grow flex flex-col">
<div className="container px-4 md:px-8 flex-grow flex flex-col">
<Outlet />
</div>
</div>
<div className="container px-4 md:px-8">
<Footer />
</div>
</>
)
}

View File

@ -0,0 +1,13 @@
import { appConfig } from "@/config/app";
import { ModeToggle } from "../mode-toggle";
export function Footer() {
return (
<footer className="flex flex-col items-center justify-between gap-4 min-h-[3rem] md:h-20 py-2 md:flex-row">
<p className="text-center text-sm leading-loose text-muted-foreground md:text-left">Built by <a href={appConfig.author.url} target="_blank" rel="noreferrer" className="font-medium underline underline-offset-4">{appConfig.author.name}</a>. The source code is available on <a href={appConfig.github.url} target="_blank" rel="noreferrer" className="font-medium underline underline-offset-4">GitHub</a>.</p>
<div className="hidden md:block">
<ModeToggle />
</div>
</footer>
)
}

View File

@ -0,0 +1,212 @@
import { useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { cn } from "@/lib/utils";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Icons } from "@/components/icons";
import { appConfig } from "@/config/app";
import { Button, buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { mainMenu } from "@/config/menu";
import { ChevronDownIcon, ViewVerticalIcon } from "@radix-ui/react-icons";
import { ScrollArea } from "@radix-ui/react-scroll-area";
import { Logo } from "../logo";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
export function Header() {
const [open, setOpen] = useState(false)
const location = useLocation();
return (
<header className="supports-backdrop-blur:bg-background/60 sticky top-0 z-50 w-full border-b bg-background/90 backdrop-blur">
<div className="container px-4 md:px-8 flex h-14 items-center">
<div className="mr-4 hidden md:flex">
<NavLink to="/" className="mr-6 flex items-center space-x-2">
<Logo />
</NavLink>
<nav className="flex items-center space-x-6 text-sm font-medium">
{mainMenu.map((menu, index) =>
menu.items !== undefined ? (
<DropdownMenu key={index}>
<DropdownMenuTrigger className={cn(
"flex items-center py-1 focus:outline-none text-sm font-medium transition-colors hover:text-primary",
(menu.items.filter(subitem => subitem.to !== undefined).map(subitem => subitem.to))
.includes(location.pathname) ? 'text-foreground' : 'text-foreground/60',
)}>
{menu.title}
<ChevronDownIcon className="ml-1 -mr-1 h-3 w-3 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className='w-48' align="start" forceMount>
{menu.items.map((subitem, subindex) =>
subitem.to !== undefined ? (
<NavLink key={subindex} to={subitem.to}>
<DropdownMenuItem className={cn(
"hover:cursor-pointer",
{ 'bg-muted': subitem.to === location.pathname }
)}>
{subitem.title}
</DropdownMenuItem>
</NavLink>
) : (
subitem.label ? (
<DropdownMenuLabel key={subindex}>{subitem.title}</DropdownMenuLabel>
) : (
<DropdownMenuSeparator key={subindex} />
)
)
)}
</DropdownMenuContent>
</DropdownMenu>
) : (
<NavLink
key={index}
to={menu.to ?? ""}
className={({ isActive }) => cn(
"text-sm font-medium transition-colors hover:text-primary",
isActive ? "text-foreground" : "text-foreground/60"
)}>
{menu.title}
</NavLink>
)
)}
</nav>
</div>
{/* mobile */}
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
className="mr-4 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden">
<ViewVerticalIcon className="h-5 w-5" />
<span className="sr-only">Toggle Menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="pr-0 sm:max-w-xs">
<NavLink
to="/"
onClick={() => setOpen(false)}
className="flex items-center space-x-2">
<Logo />
</NavLink>
<ScrollArea className="my-4 h-[calc(100vh-8rem)] pb-8 pl-8">
<Accordion type="single" collapsible className="w-full"
defaultValue={"item-" + mainMenu.findIndex(item => item.items !== undefined ? item.items.filter(subitem => subitem.to !== undefined).map(subitem => subitem.to).includes(location.pathname) : false)}>
<div className="flex flex-col space-y-3">
{mainMenu.map((menu, index) =>
menu.items !== undefined ? (
<AccordionItem key={index} value={`item-${index}`} className="border-b-0 pr-6">
<AccordionTrigger className={cn(
"py-1 hover:no-underline hover:text-primary [&[data-state=open]]:text-primary",
(menu.items.filter(subitem => subitem.to !== undefined).map(subitem => subitem.to))
.includes(location.pathname) ? 'text-foreground' : 'text-foreground/60',
)}>
<div className="flex">{menu.title}</div>
</AccordionTrigger>
<AccordionContent className="pb-1 pl-4">
<div className="mt-1">
{menu.items.map((submenu, subindex) => (
submenu.to !== undefined ? (
<NavLink
key={subindex}
to={submenu.to}
onClick={() => setOpen(false)}
className={({ isActive }) => cn(
"block justify-start py-1 h-auto font-normal hover:text-primary",
isActive ? 'text-foreground' : 'text-foreground/60',
)}>
{submenu.title}
</NavLink>
) : (
submenu.label !== '' ? (
null
) : (
<div className="px-3">
{/* <Separator /> */}
</div>
)
)
))}
</div>
</AccordionContent>
</AccordionItem>
) : (
<NavLink
key={index}
to={menu.to ?? ""}
onClick={() => setOpen(false)}
className={({ isActive }) => cn(
"py-1 text-sm font-medium transition-colors hover:text-primary",
isActive ? "text-foreground" : "text-foreground/60"
)}>
{menu.title}
</NavLink>
)
)}
</div>
</Accordion>
</ScrollArea>
</SheetContent>
</Sheet>
<a href="/" className="mr-6 flex items-center space-x-2 md:hidden">
<Icons.logo className="h-6 w-6" />
<span className="font-bold inline-block">{appConfig.name}</span>
</a>
{/* right */}
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<div className="w-full flex-1 md:w-auto md:flex-none">
{/* <CommandMenu /> */}
</div>
<nav className="flex items-center space-x-2">
<a
href={appConfig.github.url}
title={appConfig.github.title}
target="_blank"
rel="noreferrer">
<div
className={cn(
buttonVariants({
variant: "ghost",
}),
"w-9 px-0"
)}>
<Icons.gitHub className="h-4 w-4" />
<span className="sr-only">GitHub</span>
</div>
</a>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
className='relative h-8 w-8 rounded-full'>
<Avatar className='h-8 w-8'>
<AvatarFallback>SC</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-56' align='end' forceMount>
<DropdownMenuLabel className='font-normal'>
<div className='flex flex-col space-y-1'>
<p className='text-sm font-medium leading-none'>shadcn</p>
<p className='text-xs leading-none text-muted-foreground'>
m@example.com
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</nav>
</div>
</div>
</header>
)
}

View File

@ -0,0 +1,11 @@
import { appConfig } from "@/config/app";
import { Icons } from "./icons";
export function Logo() {
return (
<>
<Icons.logo className="h-6 w-6" />
<span className="font-bold">{appConfig.name}</span>
</>
)
}

View File

@ -0,0 +1,38 @@
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/hooks/useTheme"
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-9 px-0">
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="cursor-pointer" onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer" onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer" onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,53 @@
import Balance from "react-wrap-balancer"
import { cn } from "@/lib/utils"
function PageHeader({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<section
className={cn(
"pt-6 pb-4 flex items-center justify-between space-y-2",
className
)}
{...props}
>
{children}
</section>
)
}
function PageHeaderHeading({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h1
className={cn(
"text-3xl font-semibold tracking-tight my-1",
className
)}
{...props}
/>
)
}
function PageHeaderDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<Balance
className={cn(
"max-w-[750px] text-lg text-muted-foreground sm:text-xl",
className
)}
{...props}
/>
)
}
export { PageHeader, PageHeaderHeading, PageHeaderDescription }

View File

@ -0,0 +1,55 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

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

View File

@ -0,0 +1,70 @@
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,198 @@
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,203 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,162 @@
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,138 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,33 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,191 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@ -0,0 +1,567 @@
import React, { useState, useEffect } from "react"
import { createClient, WebDAVClient, FileStat } from "webdav"
import { File, Folder, ChevronRight, ChevronDown, Edit, Copy, Move, Trash, Download, Upload, AlertCircle, Loader2, FolderPlus, Maximize2, Minimize2 } from "lucide-react"
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { markdown } from '@codemirror/lang-markdown'
import { json } from '@codemirror/lang-json'
import { yaml } from '@codemirror/lang-yaml'
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { useToast } from "./ui/use-toast"
// Custom fetch function to handle CORS
async function customFetch(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
...options,
mode: 'no-cors',
credentials: 'omit',
headers: {
...options.headers,
'Content-Type': 'text/plain',
},
})
return response
}
// Initialize WebDAV client with custom fetch
const client = createClient("http://localhost:3333", {
username: "admin",
password: "1234",
}) as WebDAVClient
console.log("WebDAV client initialized:", client)
type FileSystemItem = {
basename: string
filename: string
type: "directory" | "file"
lastmod: string
size: number
}
const getFileExtension = (filename: string): string => {
return filename.split('.').pop()?.toLowerCase() || ''
}
const isTextFile = (filename: string): boolean => {
const textExtensions = ['txt', 'md', 'json', 'yaml', 'yml', 'js', 'jsx', 'ts', 'tsx', 'css', 'html', 'xml', 'csv', 'log']
return textExtensions.includes(getFileExtension(filename))
}
const isMarkdownFile = (filename: string): boolean => {
return getFileExtension(filename) === 'md'
}
const getLanguageExtension = (filename: string) => {
const ext = getFileExtension(filename)
switch (ext) {
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
return javascript()
case 'md':
return markdown()
case 'json':
return json()
case 'yaml':
case 'yml':
return yaml()
default:
return javascript() // fallback for other text files
}
}
const Breadcrumb: React.FC<{
path: string
onNavigate: (path: string) => void
}> = ({ path, onNavigate }) => {
const segments = path.split('/').filter(Boolean)
return (
<div className="flex items-center space-x-2 mb-4 text-sm">
<span
className="cursor-pointer hover:underline"
onClick={() => onNavigate("/")}
>
root
</span>
{segments.map((segment, index) => {
const pathToHere = '/' + segments.slice(0, index + 1).join('/') + '/'
return (
<React.Fragment key={pathToHere}>
<ChevronRight className="h-4 w-4" />
<span
className="cursor-pointer hover:underline"
onClick={() => onNavigate(pathToHere)}
>
{segment}
</span>
</React.Fragment>
)
})}
</div>
)
}
const FileUploader: React.FC<{ currentPath: string, onUpload: () => void }> = ({ currentPath, onUpload }) => {
const { toast } = useToast()
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files?.length) return
try {
for (const file of files) {
const reader = new FileReader()
reader.onload = async (e) => {
const content = e.target?.result
if (content) {
const path = `${currentPath}${file.name}`
console.log(`Uploading file: ${path}`)
try {
await client.putFileContents(path, content)
console.log(`File uploaded successfully: ${path}`)
toast({
title: "File Uploaded",
description: `Successfully uploaded ${file.name}`,
})
onUpload()
} catch (error) {
console.error(`Error uploading file ${path}:`, error)
toast({
title: "Upload Failed",
description: `Failed to upload ${file.name}`,
variant: "destructive",
})
}
}
}
reader.readAsArrayBuffer(file)
}
} catch (error) {
console.error("Upload error:", error)
toast({
title: "Upload Failed",
description: "Failed to upload file(s)",
variant: "destructive",
})
}
}
return (
<div className="relative">
<Input
type="file"
multiple
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileChange}
/>
<Button variant="outline" className="w-full">
<Upload className="mr-2 h-4 w-4" />
Upload Files
</Button>
</div>
)
}
const FileSystemItem: React.FC<{
item: FileSystemItem
currentPath: string
onRefresh: () => void
}> = ({ item, currentPath, onRefresh }) => {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [editedContent, setEditedContent] = useState("")
const [isFullscreen, setIsFullscreen] = useState(false)
const { toast } = useToast()
const handleEdit = async () => {
try {
console.log(`Fetching content for file: ${currentPath}${item.basename}`)
const content = await client.getFileContents(`${currentPath}${item.basename}`, { format: "text" })
console.log(`File content fetched successfully: ${currentPath}${item.basename}`)
if (isMarkdownFile(item.basename)) {
// For markdown files, open in dillinger.io
const contentStr = content as string
const encodedContent = encodeURIComponent(contentStr)
window.open(`https://dillinger.io/?content=${encodedContent}`, '_blank')
return
}
setEditedContent(content as string)
setIsEditDialogOpen(true)
} catch (error) {
console.error(`Error reading file ${currentPath}${item.basename}:`, error)
toast({
title: "Error",
description: "Failed to read file content",
variant: "destructive",
})
}
}
const handleSave = async () => {
try {
console.log(`Saving file: ${currentPath}${item.basename}`)
await client.putFileContents(`${currentPath}${item.basename}`, editedContent)
console.log(`File saved successfully: ${currentPath}${item.basename}`)
setIsEditDialogOpen(false)
setIsFullscreen(false)
toast({
title: "Success",
description: "File saved successfully",
})
onRefresh()
} catch (error) {
console.error(`Error saving file ${currentPath}${item.basename}:`, error)
toast({
title: "Error",
description: "Failed to save file",
variant: "destructive",
})
}
}
const handleDownload = async () => {
try {
console.log(`Downloading file: ${currentPath}${item.basename}`)
const content = await client.getFileContents(`${currentPath}${item.basename}`)
console.log(`File content fetched for download: ${currentPath}${item.basename}`)
const blob = new Blob([content as ArrayBuffer])
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = item.basename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
console.log(`File download initiated: ${item.basename}`)
} catch (error) {
console.error(`Download error for file ${currentPath}${item.basename}:`, error)
toast({
title: "Download Failed",
description: "Failed to download file",
variant: "destructive",
})
}
}
const handleDelete = async () => {
try {
console.log(`Deleting item: ${currentPath}${item.basename}`)
await client.deleteFile(`${currentPath}${item.basename}`)
console.log(`Item deleted successfully: ${currentPath}${item.basename}`)
toast({
title: "Success",
description: `${item.basename} deleted successfully`,
})
onRefresh()
} catch (error) {
console.error(`Delete error for item ${currentPath}${item.basename}:`, error)
toast({
title: "Delete Failed",
description: "Failed to delete item",
variant: "destructive",
})
}
}
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen)
}
return (
<ContextMenu>
<ContextMenuTrigger>
<div className="flex items-center p-2 rounded-lg cursor-pointer hover:bg-accent">
{item.type === "directory" ? (
<Folder className="mr-2 h-4 w-4" />
) : (
<File className="mr-2 h-4 w-4" />
)}
<span className="flex-1">{item.basename}</span>
<span className="text-sm text-muted-foreground">
{item.type === "file" && formatFileSize(item.size)}
</span>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{item.type === "file" && isTextFile(item.basename) && (
<ContextMenuItem onSelect={handleEdit}>
<Edit className="mr-2 h-4 w-4" />
Edit
</ContextMenuItem>
)}
{item.type === "file" && (
<ContextMenuItem onSelect={handleDownload}>
<Download className="mr-2 h-4 w-4" />
Download
</ContextMenuItem>
)}
<ContextMenuItem onSelect={handleDelete}>
<Trash className="mr-2 h-4 w-4" />
Delete
</ContextMenuItem>
</ContextMenuContent>
<Dialog
open={isEditDialogOpen}
onOpenChange={(open) => {
setIsEditDialogOpen(open)
if (!open) setIsFullscreen(false)
}}
>
<DialogContent className={cn(
"transition-all duration-200",
isFullscreen ? "w-screen h-screen max-w-none m-0 rounded-none" : "max-w-4xl"
)}>
<DialogHeader className="flex flex-row items-center justify-between">
<div>
<DialogTitle>Edit {item.basename}</DialogTitle>
<DialogDescription>Make changes to your file content here.</DialogDescription>
</div>
<Button variant="outline" size="icon" onClick={toggleFullscreen}>
{isFullscreen ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</Button>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className={cn(
"border rounded-md",
isFullscreen ? "h-[calc(100vh-200px)]" : "h-[400px]"
)}>
<CodeMirror
value={editedContent}
height={isFullscreen ? "calc(100vh - 200px)" : "400px"}
extensions={[getLanguageExtension(item.basename)]}
onChange={(value) => setEditedContent(value)}
theme="dark"
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleSave}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</ContextMenu>
)
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 B"
const k = 1024
const sizes = ["B", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
}
export function WebdavFileManager() {
const [currentPath, setCurrentPath] = useState("/")
const [items, setItems] = useState<FileSystemItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [pathHistory, setPathHistory] = useState<string[]>(["/"])
const [isNewDirDialogOpen, setIsNewDirDialogOpen] = useState(false)
const [newDirName, setNewDirName] = useState("")
const { toast } = useToast()
const loadDirectory = async (path: string) => {
setIsLoading(true)
setError(null)
try {
console.log(`Loading directory contents for path: ${path}`)
const contents = await client.getDirectoryContents(path)
console.log(`Directory contents loaded successfully for path: ${path}`, contents)
// Filter out items starting with '.'
const filteredContents = (Array.isArray(contents) ? contents : [])
.filter((item: FileStat) => !item.basename.startsWith('.'))
.map((item: FileStat) => ({
basename: item.basename,
filename: item.filename,
type: item.type,
lastmod: item.lastmod,
size: item.size,
}))
setItems(filteredContents)
} catch (error) {
console.error(`Error loading directory contents for path ${path}:`, error)
setError("Failed to load directory contents")
} finally {
setIsLoading(false)
}
}
const createNewDirectory = async () => {
if (!newDirName) return
try {
const newDirPath = `${currentPath}${newDirName}`
console.log(`Creating new directory: ${newDirPath}`)
await client.createDirectory(newDirPath)
console.log(`Directory created successfully: ${newDirPath}`)
toast({
title: "Success",
description: `Directory "${newDirName}" created successfully`,
})
setIsNewDirDialogOpen(false)
setNewDirName("")
loadDirectory(currentPath)
} catch (error) {
console.error(`Error creating directory ${newDirName}:`, error)
toast({
title: "Error",
description: "Failed to create directory",
variant: "destructive",
})
}
}
useEffect(() => {
loadDirectory(currentPath)
}, [currentPath])
const handleFolderClick = (item: FileSystemItem) => {
if (item.type === "directory") {
const newPath = `${currentPath}${item.basename}/`
console.log(`Navigating to folder: ${newPath}`)
setCurrentPath(newPath)
setPathHistory([...pathHistory, newPath])
}
}
const handleBackClick = () => {
if (pathHistory.length > 1) {
const newHistory = [...pathHistory]
newHistory.pop()
const previousPath = newHistory[newHistory.length - 1]
console.log(`Navigating back to: ${previousPath}`)
setCurrentPath(previousPath)
setPathHistory(newHistory)
}
}
const handleBreadcrumbNavigate = (path: string) => {
setCurrentPath(path)
const pathIndex = pathHistory.indexOf(path)
if (pathIndex !== -1) {
setPathHistory(pathHistory.slice(0, pathIndex + 1))
} else {
setPathHistory([...pathHistory, path])
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2 text-lg">Loading directory contents...</span>
</div>
)
}
return (
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={handleBackClick}
disabled={pathHistory.length <= 1}
>
Back
</Button>
<h1 className="text-2xl font-bold">WebDAV File Manager</h1>
</div>
<FileUploader currentPath={currentPath} onUpload={() => loadDirectory(currentPath)} />
</div>
<Breadcrumb path={currentPath} onNavigate={handleBreadcrumbNavigate} />
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="border rounded-lg">
<ContextMenu>
<ContextMenuTrigger>
<ScrollArea className="h-[600px]">
<div className="p-4">
{items.map((item) => (
<div
key={item.filename}
onClick={() => handleFolderClick(item)}
>
<FileSystemItem
item={item}
currentPath={currentPath}
onRefresh={() => loadDirectory(currentPath)}
/>
</div>
))}
</div>
</ScrollArea>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={() => setIsNewDirDialogOpen(true)}>
<FolderPlus className="mr-2 h-4 w-4" />
New Directory
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
<Dialog open={isNewDirDialogOpen} onOpenChange={setIsNewDirDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Directory</DialogTitle>
<DialogDescription>Enter a name for the new directory.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Label htmlFor="dirname">Directory Name</Label>
<Input
id="dirname"
value={newDirName}
onChange={(e) => setNewDirName(e.target.value)}
placeholder="Enter directory name"
/>
</div>
<DialogFooter>
<Button onClick={createNewDirectory}>Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@ -0,0 +1,23 @@
interface AppConfig {
name: string,
github: {
title: string,
url: string
},
author: {
name: string,
url: string
},
}
export const appConfig: AppConfig = {
name: "Sample App",
github: {
title: "React Shadcn Starter",
url: "https://github.com/hayyi2/react-shadcn-starter",
},
author: {
name: "hayyi",
url: "https://github.com/hayyi2/",
}
}

View File

@ -0,0 +1,41 @@
import { Icons } from "@/components/icons"
interface NavItem {
title: string
to?: string
href?: string
disabled?: boolean
external?: boolean
icon?: keyof typeof Icons
label?: string
}
interface NavItemWithChildren extends NavItem {
items?: NavItemWithChildren[]
}
export const mainMenu: NavItemWithChildren[] = [
{
title: 'Dashboard',
to: '',
},
{
title: 'Dropdown',
items: [
{
title: 'Sample',
to: '/sample',
},
{
title: 'Sample Dua',
to: '/#',
},
]
},
{
title: 'Empty',
to: 'empty',
},
]
export const sideMenu: NavItemWithChildren[] = []

View File

@ -0,0 +1,60 @@
import { createContext, useEffect, useState } from "react"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: string
storageKey?: string
}
export type ThemeProviderState = {
theme: string
setTheme: (theme: string) => void
}
const initialState = {
theme: "system",
setTheme: () => null,
}
export const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "shadcn-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState(
() => localStorage.getItem(storageKey) ?? defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
return (
<ThemeProviderContext.Provider {...props} value={{
theme,
setTheme: (theme: string) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}}>
{children}
</ThemeProviderContext.Provider>
)
}

View File

@ -0,0 +1,191 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@ -0,0 +1,11 @@
import { useContext } from "react";
import { ThemeProviderContext, ThemeProviderState } from "../contexts/ThemeContext";
export function useTheme(): ThemeProviderState {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
}

View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,162 @@
import md5 from 'md5';
export type WebDAVConfig = {
url: string;
username: string;
password: string;
}
export type Event = {
id: string;
title: string;
date: string;
time: string;
duration: number;
category: string;
color: string;
content: string;
attendees: string[];
isFullDay: boolean;
}
export const categories = [
{ name: 'Work', color: 'bg-red-500 dark:bg-red-700' },
{ name: 'Personal', color: 'bg-blue-500 dark:bg-blue-700' },
{ name: 'Family', color: 'bg-green-500 dark:bg-green-700' },
];
export const defaultEvents: Event[] = [
{
id: "1",
title: "Team Meeting",
date: "2024-11-01",
time: "10:00",
duration: 60,
category: "Work",
color: "bg-red-500 dark:bg-red-700",
content: "# Agenda\n\n- Project updates\n- Q4 planning\n- Team building activities",
attendees: ["john@example.com", "sarah@example.com"],
isFullDay: false
},
{
id: "2",
title: "Dentist Appointment",
date: "2024-11-03",
time: "14:30",
duration: 45,
category: "Personal",
color: "bg-blue-500 dark:bg-blue-700",
content: "Regular checkup and cleaning",
attendees: [],
isFullDay: false
},
{
id: "3",
title: "Family Dinner",
date: "2024-11-05",
time: "18:00",
duration: 120,
category: "Family",
color: "bg-green-500 dark:bg-green-700",
content: "Dinner at grandma's house\n\n- Bring dessert\n- Don't forget family photos",
attendees: ["mom@example.com", "dad@example.com", "sister@example.com"],
isFullDay: false
}
];
export class CalendarDataManager {
private static instance: CalendarDataManager;
private currentHash: string | null = null;
private webdavConfig: WebDAVConfig | null = null;
private calendarFile: string | null = null;
private constructor() {}
static getInstance(): CalendarDataManager {
if (!CalendarDataManager.instance) {
CalendarDataManager.instance = new CalendarDataManager();
}
return CalendarDataManager.instance;
}
setConfig(config: WebDAVConfig, calendarFile: string) {
this.webdavConfig = config;
this.calendarFile = calendarFile;
}
private calculateHash(events: Event[]): string {
// Sort events by ID to ensure consistent hash
const sortedEvents = [...events].sort((a, b) => a.id.localeCompare(b.id));
return md5(JSON.stringify(sortedEvents));
}
hasChanged(events: Event[]): boolean {
const newHash = this.calculateHash(events);
if (newHash === this.currentHash) {
return false;
}
this.currentHash = newHash;
return true;
}
async fetchEvents(): Promise<Event[]> {
if (!this.webdavConfig || !this.calendarFile) {
throw new Error('WebDAV configuration or calendar file not set');
}
const response = await fetch(`${this.webdavConfig.url}${this.calendarFile}`, {
headers: {
'Authorization': 'Basic ' + btoa(`${this.webdavConfig.username}:${this.webdavConfig.password}`)
}
});
if (!response.ok) {
throw new Error(`Failed to fetch calendar data: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.events;
}
async saveEvents(events: Event[]): Promise<void> {
if (!this.webdavConfig || !this.calendarFile) {
throw new Error('WebDAV configuration or calendar file not set');
}
const response = await fetch(`${this.webdavConfig.url}${this.calendarFile}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${this.webdavConfig.username}:${this.webdavConfig.password}`)
},
body: JSON.stringify({ events }, null, 2)
});
if (!response.ok) {
throw new Error('Failed to save calendar data');
}
this.currentHash = this.calculateHash(events);
}
generateTimeOptions(): string[] {
const times: string[] = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 15) {
const formattedHour = hour.toString().padStart(2, '0');
const formattedMinute = minute.toString().padStart(2, '0');
times.push(`${formattedHour}:${formattedMinute}`);
}
}
return times;
}
formatDuration(minutes: number): string {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (hours === 0) return `${minutes} minutes`;
return `${hours}h${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ''}`;
}
}
export const timeOptions = CalendarDataManager.getInstance().generateTimeOptions();

View File

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

View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@ -0,0 +1,18 @@
import { PageHeader, PageHeaderHeading } from "@/components/page-header";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function Dashboard() {
return (
<>
<PageHeader>
<PageHeaderHeading>Dashboard</PageHeaderHeading>
</PageHeader>
<Card>
<CardHeader>
<CardTitle>React Shadcn Starter</CardTitle>
<CardDescription>React + Vite + TypeScript template for building apps with shadcn/ui.</CardDescription>
</CardHeader>
</Card>
</>
)
}

View File

@ -0,0 +1,18 @@
import { PageHeader, PageHeaderHeading } from "@/components/page-header";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function Empty() {
return (
<>
<PageHeader>
<PageHeaderHeading>Empty Page</PageHeaderHeading>
</PageHeader>
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description.</CardDescription>
</CardHeader>
</Card>
</>
)
}

View File

@ -0,0 +1,15 @@
import { buttonVariants } from "@/components/ui/button";
import { NavLink } from "react-router-dom";
export default function NoMatch() {
return (
<div className="bg-background text-foreground flex-grow flex items-center justify-center">
<div className="space-y-4">
<h2 className="text-8xl mb-4">404</h2>
<h1 className="text-3xl font-semibold">Oops! Page not found</h1>
<p className="text-sm text-muted-foreground">We are sorry, but the page you requested was not found</p>
<NavLink to="/" className={buttonVariants()}>Back to Home</NavLink>
</div>
</div>
)
}

View File

@ -0,0 +1,18 @@
import { PageHeader, PageHeaderHeading } from "@/components/page-header";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function Sample() {
return (
<>
<PageHeader>
<PageHeaderHeading>Sample Page</PageHeaderHeading>
</PageHeader>
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description.</CardDescription>
</CardHeader>
</Card>
</>
)
}

View File

@ -0,0 +1,5 @@
/// <reference types="vite/client" />
declare const global: {
basename: string
}

View File

@ -0,0 +1,76 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})

View File

@ -0,0 +1 @@
:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}.card{padding:2em}#app{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}

File diff suppressed because one or more lines are too long

14
svelte/myfiles/dist/index.html vendored Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte</title>
<script type="module" crossorigin src="/assets/index-DliVx7m2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DPDjIYFo.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
svelte/myfiles/dist/vite.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
webdavserver/install.sh Normal file
View File

@ -0,0 +1 @@
pip install wsgidav cheroot

2
webdavserver/readme.md Normal file
View File

@ -0,0 +1,2 @@
https://wsgidav.readthedocs.io/en/latest/user_guide_configure.html

3
webdavserver/run.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
wsgidav -c wsgidav.yaml

36
webdavserver/wsgidav.yaml Normal file
View File

@ -0,0 +1,36 @@
server: cheroot
host: 0.0.0.0
port: 3333
verbose: 3
provider_mapping:
"/":
root: "/tmp/webdav"
dir_browser:
enable: true
icon: true
response_handler: true
http_authenticator:
domain_controller: null # Same as wsgidav.dc.simple_dc.SimpleDomainController
accept_basic: true # Pass false to prevent sending clear text passwords
accept_digest: true
default_to_digest: true
simple_dc:
user_mapping:
"*":
"admin":
password: "1234"
"user2":
password: "qwerty"
"/pub": true
cors:
allow_origin: '*'
allow_methods: '*'
allow_headers: '*'
expose_headers: '*'
allow_credentials: true
max_age: 600