...
This commit is contained in:
parent
676e2d2c0e
commit
8d9018722e
152
examples/images/_archive/company copy.html
Normal file
152
examples/images/_archive/company copy.html
Normal 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>
|
301
examples/images/company.html
Normal file
301
examples/images/company.html
Normal 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
51
examples/images/data.json
Normal 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
112
examples/images/home.css
Normal 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);
|
||||||
|
}
|
7
examples/images/home.html
Normal file
7
examples/images/home.html
Normal 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
100
examples/images/index.html
Normal 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
291
examples/images/index2.html
Normal 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()">×</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
296
examples/images/index3.html
Normal 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()">×</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>
|
101
examples/images/index_good.html
Normal file
101
examples/images/index_good.html
Normal 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>
|
340
examples/images/index_images_random_zoom.html
Normal file
340
examples/images/index_images_random_zoom.html
Normal 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()">×</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>
|
57
examples/images/index_notworking.html
Normal file
57
examples/images/index_notworking.html
Normal 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
31
examples/images/menu.html
Normal 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>
|
33
examples/images/navigation.html
Normal file
33
examples/images/navigation.html
Normal 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
52
examples/images/script.js
Normal 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
19
examples/images/server.py
Normal 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
5
examples/images/start.sh
Executable 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
94
react-shadcn-starter/.gitignore
vendored
Normal 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
|
21
react-shadcn-starter/LICENSE
Normal file
21
react-shadcn-starter/LICENSE
Normal 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.
|
77
react-shadcn-starter/README.md
Normal file
77
react-shadcn-starter/README.md
Normal 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.
|
16
react-shadcn-starter/components.json
Normal file
16
react-shadcn-starter/components.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
13
react-shadcn-starter/index.html
Normal file
13
react-shadcn-starter/index.html
Normal 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
12033
react-shadcn-starter/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
react-shadcn-starter/package.json
Normal file
61
react-shadcn-starter/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
6
react-shadcn-starter/postcss.config.js
Normal file
6
react-shadcn-starter/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
3
react-shadcn-starter/public/favicon.svg
Normal file
3
react-shadcn-starter/public/favicon.svg
Normal 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 |
26
react-shadcn-starter/src/App.tsx
Normal file
26
react-shadcn-starter/src/App.tsx
Normal 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
|
35
react-shadcn-starter/src/Router.tsx
Normal file
35
react-shadcn-starter/src/Router.tsx
Normal 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
|
||||||
|
})
|
535
react-shadcn-starter/src/components/calendar.tsx
Normal file
535
react-shadcn-starter/src/components/calendar.tsx
Normal 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 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>
|
||||||
|
)
|
||||||
|
}
|
17
react-shadcn-starter/src/components/icons.tsx
Normal file
17
react-shadcn-starter/src/components/icons.tsx
Normal 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>
|
||||||
|
),
|
||||||
|
}
|
19
react-shadcn-starter/src/components/layouts/AppLayout.tsx
Normal file
19
react-shadcn-starter/src/components/layouts/AppLayout.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
13
react-shadcn-starter/src/components/layouts/Footer.tsx
Normal file
13
react-shadcn-starter/src/components/layouts/Footer.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
212
react-shadcn-starter/src/components/layouts/Header.tsx
Normal file
212
react-shadcn-starter/src/components/layouts/Header.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
11
react-shadcn-starter/src/components/logo.tsx
Normal file
11
react-shadcn-starter/src/components/logo.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
38
react-shadcn-starter/src/components/mode-toggle.tsx
Normal file
38
react-shadcn-starter/src/components/mode-toggle.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
53
react-shadcn-starter/src/components/page-header.tsx
Normal file
53
react-shadcn-starter/src/components/page-header.tsx
Normal 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 }
|
55
react-shadcn-starter/src/components/ui/accordion.tsx
Normal file
55
react-shadcn-starter/src/components/ui/accordion.tsx
Normal 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 }
|
59
react-shadcn-starter/src/components/ui/alert.tsx
Normal file
59
react-shadcn-starter/src/components/ui/alert.tsx
Normal 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 }
|
48
react-shadcn-starter/src/components/ui/avatar.tsx
Normal file
48
react-shadcn-starter/src/components/ui/avatar.tsx
Normal 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 }
|
56
react-shadcn-starter/src/components/ui/button.tsx
Normal file
56
react-shadcn-starter/src/components/ui/button.tsx
Normal 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 }
|
70
react-shadcn-starter/src/components/ui/calendar.tsx
Normal file
70
react-shadcn-starter/src/components/ui/calendar.tsx
Normal 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 }
|
76
react-shadcn-starter/src/components/ui/card.tsx
Normal file
76
react-shadcn-starter/src/components/ui/card.tsx
Normal 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 }
|
198
react-shadcn-starter/src/components/ui/context-menu.tsx
Normal file
198
react-shadcn-starter/src/components/ui/context-menu.tsx
Normal 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,
|
||||||
|
}
|
120
react-shadcn-starter/src/components/ui/dialog.tsx
Normal file
120
react-shadcn-starter/src/components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
203
react-shadcn-starter/src/components/ui/dropdown-menu.tsx
Normal file
203
react-shadcn-starter/src/components/ui/dropdown-menu.tsx
Normal 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,
|
||||||
|
}
|
25
react-shadcn-starter/src/components/ui/input.tsx
Normal file
25
react-shadcn-starter/src/components/ui/input.tsx
Normal 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 }
|
24
react-shadcn-starter/src/components/ui/label.tsx
Normal file
24
react-shadcn-starter/src/components/ui/label.tsx
Normal 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 }
|
33
react-shadcn-starter/src/components/ui/popover.tsx
Normal file
33
react-shadcn-starter/src/components/ui/popover.tsx
Normal 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 }
|
46
react-shadcn-starter/src/components/ui/scroll-area.tsx
Normal file
46
react-shadcn-starter/src/components/ui/scroll-area.tsx
Normal 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 }
|
162
react-shadcn-starter/src/components/ui/select.tsx
Normal file
162
react-shadcn-starter/src/components/ui/select.tsx
Normal 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,
|
||||||
|
}
|
138
react-shadcn-starter/src/components/ui/sheet.tsx
Normal file
138
react-shadcn-starter/src/components/ui/sheet.tsx
Normal 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,
|
||||||
|
}
|
29
react-shadcn-starter/src/components/ui/switch.tsx
Normal file
29
react-shadcn-starter/src/components/ui/switch.tsx
Normal 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 }
|
24
react-shadcn-starter/src/components/ui/textarea.tsx
Normal file
24
react-shadcn-starter/src/components/ui/textarea.tsx
Normal 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 }
|
127
react-shadcn-starter/src/components/ui/toast.tsx
Normal file
127
react-shadcn-starter/src/components/ui/toast.tsx
Normal 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,
|
||||||
|
}
|
33
react-shadcn-starter/src/components/ui/toaster.tsx
Normal file
33
react-shadcn-starter/src/components/ui/toaster.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
191
react-shadcn-starter/src/components/ui/use-toast.ts
Normal file
191
react-shadcn-starter/src/components/ui/use-toast.ts
Normal 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 }
|
567
react-shadcn-starter/src/components/webdav-file-manager.tsx
Normal file
567
react-shadcn-starter/src/components/webdav-file-manager.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
23
react-shadcn-starter/src/config/app.ts
Normal file
23
react-shadcn-starter/src/config/app.ts
Normal 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/",
|
||||||
|
}
|
||||||
|
}
|
41
react-shadcn-starter/src/config/menu.ts
Normal file
41
react-shadcn-starter/src/config/menu.ts
Normal 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[] = []
|
60
react-shadcn-starter/src/contexts/ThemeContext.tsx
Normal file
60
react-shadcn-starter/src/contexts/ThemeContext.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
191
react-shadcn-starter/src/hooks/use-toast.ts
Normal file
191
react-shadcn-starter/src/hooks/use-toast.ts
Normal 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 }
|
11
react-shadcn-starter/src/hooks/useTheme.tsx
Normal file
11
react-shadcn-starter/src/hooks/useTheme.tsx
Normal 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;
|
||||||
|
}
|
76
react-shadcn-starter/src/index.css
Normal file
76
react-shadcn-starter/src/index.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
162
react-shadcn-starter/src/lib/calendar-data.ts
Normal file
162
react-shadcn-starter/src/lib/calendar-data.ts
Normal 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();
|
6
react-shadcn-starter/src/lib/utils.ts
Normal file
6
react-shadcn-starter/src/lib/utils.ts
Normal 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))
|
||||||
|
}
|
10
react-shadcn-starter/src/main.tsx
Normal file
10
react-shadcn-starter/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
18
react-shadcn-starter/src/pages/Dashboard.tsx
Normal file
18
react-shadcn-starter/src/pages/Dashboard.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
18
react-shadcn-starter/src/pages/Empty.tsx
Normal file
18
react-shadcn-starter/src/pages/Empty.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
15
react-shadcn-starter/src/pages/NoMatch.tsx
Normal file
15
react-shadcn-starter/src/pages/NoMatch.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
18
react-shadcn-starter/src/pages/Sample.tsx
Normal file
18
react-shadcn-starter/src/pages/Sample.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
5
react-shadcn-starter/src/vite-env.d.ts
vendored
Normal file
5
react-shadcn-starter/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const global: {
|
||||||
|
basename: string
|
||||||
|
}
|
76
react-shadcn-starter/tailwind.config.js
Normal file
76
react-shadcn-starter/tailwind.config.js
Normal 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")],
|
||||||
|
}
|
29
react-shadcn-starter/tsconfig.json
Normal file
29
react-shadcn-starter/tsconfig.json
Normal 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" }]
|
||||||
|
}
|
9
react-shadcn-starter/tsconfig.node.json
Normal file
9
react-shadcn-starter/tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
13
react-shadcn-starter/vite.config.ts
Normal file
13
react-shadcn-starter/vite.config.ts
Normal 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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
1
svelte/myfiles/dist/assets/index-DPDjIYFo.css
vendored
Normal file
1
svelte/myfiles/dist/assets/index-DPDjIYFo.css
vendored
Normal 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}}
|
199
svelte/myfiles/dist/assets/index-DliVx7m2.js
vendored
Normal file
199
svelte/myfiles/dist/assets/index-DliVx7m2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
svelte/myfiles/dist/index.html
vendored
Normal file
14
svelte/myfiles/dist/index.html
vendored
Normal 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
1
svelte/myfiles/dist/vite.svg
vendored
Normal 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
1
webdavserver/install.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
pip install wsgidav cheroot
|
2
webdavserver/readme.md
Normal file
2
webdavserver/readme.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
https://wsgidav.readthedocs.io/en/latest/user_guide_configure.html
|
||||||
|
|
3
webdavserver/run.sh
Executable file
3
webdavserver/run.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
|
||||||
|
wsgidav -c wsgidav.yaml
|
36
webdavserver/wsgidav.yaml
Normal file
36
webdavserver/wsgidav.yaml
Normal 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
|
Loading…
Reference in New Issue
Block a user