UI requirements, component specifications, and technology stack.
------
**Blade + Alpine.js + Tailwind CSS**
---
work_hours_start (default: 08:00)work_hours_end (default: 18:00)work_days (default: Mon-Fri)---
javascript
Alpine.data('orgSwitcher', () => ({
organizations: [],
current: null,
async switch(orgId) {
await fetch(/api/organizations/${orgId}/switch, { method: 'POST' });
window.location.reload();
}
}));
Calendar Component
javascript
Alpine.data('calendar', () => ({
calendars: [], // enabled calendars
events: [],
scrollPosition: 0, // horizontal scroll state
filters: {
calendars: [], // selected calendar IDs
users: [], // filter by team member
},
async loadEvents() {
const params = new URLSearchParams({
start: this.range.start,
end: this.range.end,
calendars: this.filters.calendars.join(','),
});
const response = await fetch(/api/time-blocks?${params});
this.events = await response.json().data;
},
scrollToDay(day) {
// Smooth scroll to specific day
},
scrollToToday() {
// Center today in view
}
}));
Time Block Component
javascript
Alpine.data('timeBlock', (block) => ({
block: block,
isDragging: false,
isResizing: false,
init() {
this.setupDragAndDrop();
},
setupDragAndDrop() {
// interact.js configuration
},
async save() {
await fetch(/api/time-blocks/${this.block.id}, {
method: 'PUT',
body: JSON.stringify(this.block)
});
}
}));
Week Picker Component
javascript
Alpine.data('weekPicker', () => ({
currentDate: new Date(),
get weekStart() {
// Calculate Monday of current week
},
get weekEnd() {
// Calculate Sunday of current week
},
previousWeek() {
this.currentDate.setDate(this.currentDate.getDate() - 7);
this.loadEvents();
},
nextWeek() {
this.currentDate.setDate(this.currentDate.getDate() + 7);
this.loadEvents();
},
goToToday() {
this.currentDate = new Date();
this.loadEvents();
}
}));
Summary Row
javascript
Alpine.data('summaryRow', () => ({
hoursByDay: {},
totalHours: 0,
calculate(events) {
// Only count danwel events (not external)
const wpEvents = events.filter(e => e.source === 'weekplanner');
this.hoursByDay = {};
wpEvents.forEach(event => {
const day = event.start_time.split('T')[0];
const hours = this.calculateDuration(event);
this.hoursByDay[day] = (this.hoursByDay[day] || 0) + hours;
});
this.totalHours = Object.values(this.hoursByDay)
.reduce((sum, h) => sum + h, 0);
}
}));
---
View Templates
Layout Structure
resources/views/
├── layouts/
│ └── app.blade.php # Main layout with nav, org switcher
├── auth/
│ ├── login.blade.php
│ ├── register.blade.php
│ └── invitation.blade.php
├── calendar/
│ └── index.blade.php # Main calendar view
├── components/
│ ├── calendar-grid.blade.php
│ ├── org-switcher.blade.php
│ ├── time-block.blade.php
│ ├── week-picker.blade.php
│ └── summary-row.blade.php
├── dashboard.blade.php
├── organizations/
│ ├── create.blade.php
│ └── settings.blade.php
├── settings/
│ ├── index.blade.php
│ ├── integrations.blade.php
│ └── team.blade.php
└── team/
├── index.blade.php
└── invite.blade.php
---
Mobile & Tablet Support (Phase 2)
Approach Options
| Option | Effort | UX | Offline |
|--------|--------|-----|---------|
| **Responsive PWA** | Low | Good | Basic (service worker) |
| **React Native** | High | Excellent | Full |
| **Flutter** | High | Excellent | Full |
| **Capacitor (wrap web)** | Medium | Good | Partial |**Recommendation**: Start with **Responsive PWA** for Phase 2, consider native apps only if there's strong demand.
PWA Features to Add
javascript
// manifest.json
{
"name": "danwel",
"short_name": "danwel",
"display": "standalone",
"start_url": "/calendar",
"theme_color": "#4F46E5",
"icons": [...]
}
// Service worker for offline calendar viewing
// Push notifications for reminders
// Touch-optimized drag-drop
API Ready for Mobile
The REST API design already supports mobile clients:
• Token-based auth via Sanctum
• JSON responses with proper pagination
• Organization context via X-Organization-Id header
• Stateless design
UI Considerations for Touch
• Larger touch targets (44px minimum)
• Swipe gestures for week navigation
• Bottom sheet modals instead of centered
• Simplified calendar view for small screens ---
Prototype Reference
The current prototype (
calendar.php) contains working JavaScript that should be preserved:Working Features from Prototype
| Feature | Implementation |
|---------|----------------|
| **loadBlocksFromGoogle()** | Convert Google events to local blocks array |
| **renderBlocks()** | Render time blocks on calendar grid |
| **setupDragAndResize()** | Initialize interact.js handlers |
| **apiCreate/Update/Delete()** | API calls for CRUD operations |
| **openModal()** | Edit/create modal dialog |
| **updateSummary()** | Calculate hours per day/week |
| **renderWeekPicker()** | Mini calendar for week selection |
Color Mapping
Google Calendar colorId to hex colors (from prototype):
| colorId | Color Name | Hex |
|---------|------------|-----|
| 1 | Lavender | #7986cb |
| 2 | Sage | #33b679 |
| 3 | Grape | #8e24aa |
| 4 | Flamingo | #e67c73 |
| 5 | Banana | #f6c026 |
| 6 | Tangerine | #f5511d |
| 7 | Peacock | #039be5 |
| 8 | Graphite | #616161 |
| 9 | Blueberry | #3f51b5 |
| 10 | Basil | #0b8043 |
| 11 | Tomato | #d60000 |
interact.js Configuration
javascript
interact('.time-block')
.draggable({
inertia: true,
modifiers: [
interact.modifiers.snap({
targets: [/* 15-minute grid */],
range: Infinity,
relativePoints: [{ x: 0, y: 0 }]
}),
interact.modifiers.restrict({
restriction: '.calendar-grid',
endOnly: true
})
],
autoScroll: true,
listeners: {
move: dragMoveListener,
end: dragEndListener
}
})
.resizable({
edges: { top: true, bottom: true },
modifiers: [
interact.modifiers.snap({
targets: [/* 15-minute increments */]
})
],
listeners: {
move: resizeMoveListener,
end: resizeEndListener
}
});
``