Skip to content

Commit 501045c

Browse files
committed
Redesign schedule page: vertical grid with rooms as columns
Replace the day dropdown and horizontal carousel with visible day tabs and a table layout with time slots on the left and rooms as column headers (Google Calendar style). Add new partial _schedule_grid.html.haml with rowspan logic per event and responsive styles in moraga-schedule.scss.
1 parent 3c567ea commit 501045c

File tree

3 files changed

+215
-59
lines changed

3 files changed

+215
-59
lines changed

app/assets/stylesheets/moraga-schedule.scss

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,138 @@ td.no-padding{
322322
font-size: 14px;
323323
}
324324
}
325+
326+
// ===== NEW VERTICAL GRID LAYOUT =====
327+
328+
.schedule-day-tabs {
329+
margin: 20px 0 0;
330+
display: flex;
331+
flex-wrap: wrap;
332+
gap: 4px;
333+
334+
li a {
335+
border-radius: 20px;
336+
padding: 6px 16px;
337+
font-size: 13px;
338+
font-weight: 500;
339+
}
340+
}
341+
342+
.schedule-grid-wrapper {
343+
overflow-x: auto;
344+
margin-top: 16px;
345+
}
346+
347+
.schedule-grid {
348+
width: 100%;
349+
border-collapse: collapse;
350+
table-layout: fixed;
351+
352+
th.time-col-header {
353+
width: 56px;
354+
min-width: 56px;
355+
}
356+
357+
th.room-header {
358+
background: #3a3a3a;
359+
color: #fff;
360+
text-align: center;
361+
padding: 8px 6px;
362+
font-size: 13px;
363+
font-weight: 600;
364+
border: 1px solid #555;
365+
min-width: 130px;
366+
}
367+
368+
td.time-cell {
369+
width: 56px;
370+
min-width: 56px;
371+
vertical-align: top;
372+
padding: 2px 6px 0;
373+
background: #f8f8f8;
374+
border-right: 2px solid #ddd;
375+
white-space: nowrap;
376+
377+
.hour-label {
378+
font-size: 12px;
379+
font-weight: 700;
380+
color: #333;
381+
}
382+
383+
.half-label {
384+
font-size: 11px;
385+
color: #999;
386+
}
387+
}
388+
389+
td.empty-cell {
390+
height: 20px;
391+
border: 1px solid #f0f0f0;
392+
background: #fafafa;
393+
}
394+
395+
td.event-cell {
396+
vertical-align: top;
397+
padding: 0;
398+
border: 1px solid #c8e6c9;
399+
background: linear-gradient(160deg, #f1f8e9 0%, #c8e6c9 100%);
400+
cursor: pointer;
401+
transition: background 0.15s;
402+
403+
&:hover {
404+
background: linear-gradient(160deg, #dcedc8 0%, #81c784 100%);
405+
}
406+
}
407+
408+
a.event-link {
409+
display: block;
410+
padding: 5px 7px;
411+
color: inherit;
412+
text-decoration: none;
413+
height: 100%;
414+
415+
.event-time {
416+
font-size: 10px;
417+
color: #555;
418+
margin-bottom: 2px;
419+
}
420+
421+
.event-title {
422+
font-size: 12px;
423+
font-weight: 600;
424+
line-height: 1.3;
425+
overflow: hidden;
426+
display: -webkit-box;
427+
-webkit-box-orient: vertical;
428+
-webkit-line-clamp: 3;
429+
}
430+
431+
.event-speakers {
432+
font-size: 10px;
433+
color: #666;
434+
margin-top: 4px;
435+
display: flex;
436+
align-items: center;
437+
gap: 4px;
438+
overflow: hidden;
439+
white-space: nowrap;
440+
text-overflow: ellipsis;
441+
442+
.speaker-avatar {
443+
width: 20px;
444+
height: 20px;
445+
border-radius: 50%;
446+
flex-shrink: 0;
447+
}
448+
}
449+
}
450+
}
451+
452+
// Responsive: smaller fonts on mobile
453+
@media (max-width: 768px) {
454+
.schedule-grid {
455+
th.room-header { font-size: 11px; padding: 6px 4px; min-width: 100px; }
456+
a.event-link .event-title { font-size: 11px; -webkit-line-clamp: 2; }
457+
td.time-cell { width: 44px; min-width: 44px; font-size: 10px; }
458+
}
459+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
- step_int = @conference.program.schedule_interval
2+
- conf_start_dt = DateTime.parse("#{date} #{@conf_start}:00")
3+
- conf_end_dt = DateTime.parse("#{date} #{@conf_start + @conf_period}:00")
4+
5+
/ Pre-build time slots
6+
- time_slots = []
7+
- slot = conf_start_dt
8+
- while slot < conf_end_dt
9+
- time_slots << slot
10+
- slot = slot + step_int.minutes
11+
12+
/ Track pending rowspan per room: how many rows still to skip
13+
- remaining_span = {}
14+
- @rooms.each { |r| remaining_span[r.id] = 0 }
15+
16+
/ Events per room filtered to this day
17+
- events_by_room = {}
18+
- @rooms.each do |room|
19+
- events_by_room[room.id] = (@event_schedules_by_room_id[room.id] || []).select { |es| es.start_time.to_date == date }
20+
21+
.schedule-grid-wrapper
22+
%table.schedule-grid
23+
%thead
24+
%tr
25+
%th.time-col-header
26+
- @rooms.each do |room|
27+
%th.room-header= room.name
28+
29+
%tbody
30+
- time_slots.each do |slot|
31+
%tr{ data: { time: slot.strftime('%H:%M') } }
32+
/ Time column (show label only on :00 and :30)
33+
%td.time-cell
34+
- if slot.min == 0
35+
%span.hour-label= slot.strftime('%H:%M')
36+
- elsif slot.min == 30
37+
%span.half-label= slot.strftime('%H:%M')
38+
39+
/ One cell per room
40+
- @rooms.each do |room|
41+
- if remaining_span[room.id] > 0
42+
/ Cell covered by an active rowspan — do not render a td
43+
- remaining_span[room.id] -= 1
44+
- else
45+
/ Find event active at this slot for this room
46+
- es = events_by_room[room.id].find { |e| e.start_time <= slot && e.end_time > slot }
47+
/ Handle canceled/withdrawn events with a confirmed replacement
48+
- if es && (es.event.state == 'canceled' || es.event.state == 'withdrawn') && es.intersecting_event_schedules.confirmed.exists?
49+
- replacement = es.intersecting_event_schedules.confirmed.first
50+
- es = (replacement.start_time <= slot && replacement.end_time > slot) ? replacement : nil
51+
- if es && es.start_time == slot
52+
/ New event starting at this slot
53+
- rowspan = ((es.end_time.to_i - slot.to_i) / 60 / step_int).round
54+
- rowspan = [rowspan, 1].max
55+
- remaining_span[room.id] = rowspan - 1
56+
%td.event-cell{ rowspan: rowspan }
57+
%a.event-link{ href: conference_program_proposal_path(@conference.short_title, es.event.id) }
58+
.event-time= "#{es.start_time.strftime('%H:%M')}#{es.end_time.strftime('%H:%M')}"
59+
.event-title= es.event.title
60+
- if es.event.speakers_ordered.any?
61+
.event-speakers
62+
- es.event.speakers_ordered.first(2).each do |speaker|
63+
= image_tag speaker.gravatar_url, class: 'speaker-avatar'
64+
= es.event.speaker_names
65+
- else
66+
/ Empty cell (no event or event already in progress from before grid start)
67+
%td.empty-cell

app/views/schedules/show.html.haml

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
11
.container
2-
= render partial: 'schedule_tabs', locals: { active: 'schedule' }
2+
= render partial: 'schedule_tabs', locals: { active: 'schedule' }
33

44
#schedule-content
55
%h1.text-center
66
= t('Schedule for')
77
= @conference.title
8-
.dropdown.schedule-dropdown
9-
%button{ type: "button", class: "btn btn-default dropdown-toggle", 'data-toggle' => "dropdown" }
10-
= @day
11-
%span.caret
12-
%ul.dropdown-menu
13-
- @dates.each do |date|
14-
%li.li-dropdown-schedule{ class: "#{ 'active' if @day == date }" }
15-
= link_to date, "#" + "#{date}", "data-toggle" => "tab", "class" => "date-tab"
168

17-
.tab-content
9+
/ Day tabs (replace dropdown)
10+
%ul.nav.nav-pills.schedule-day-tabs
1811
- @dates.each do |date|
19-
%div{ class: "tab-pane #{ 'active' if @day == date }", id: "#{ date }" }
20-
21-
.visible-xs-inline
22-
= render partial: 'carousel', locals: { date: date, hrs_per_slide: 1 }
12+
%li{ class: ('active' if @day == date) }
13+
= link_to l(date, format: :short), "##{date}", "data-toggle" => "tab", class: "day-tab"
2314

24-
.visible-sm-inline
25-
= render partial: 'carousel', locals: { date: date, hrs_per_slide: 2 }
15+
.tab-content
16+
- @dates.each do |date|
17+
%div{ class: "tab-pane #{'active' if @day == date}", id: "#{date}" }
18+
= render partial: 'schedule_grid', locals: { date: date }
2619

27-
.visible-md-inline.visible-lg-inline
28-
= render partial: 'carousel', locals: { date: date, hrs_per_slide: 3 }
2920
%p
3021
%span
3122
= link_to conference_schedule_url(protocol: 'webcal', format: 'ics') do
@@ -35,46 +26,9 @@
3526
= t('Get the mobile app')
3627

3728
:javascript
38-
// change of active tab and the button title when a date is clicked
39-
$(function() {
40-
$('.date-tab').on('click', function(e) {
41-
$('.li-dropdown-schedule').removeClass('active');
42-
$('.schedule-dropdown').find('button').text($(this).text());
43-
});
44-
});
45-
46-
// hide the right and left controls when neccesary after moving the carousel
47-
$('.carousel').on('slid.bs.carousel', '',
48-
function(){
49-
$(this).children('.left.carousel-control').show();
50-
$(this).children('.right.carousel-control').show();
51-
if($(this).find('.first').hasClass('active')) {
52-
$(this).children('.left.carousel-control').hide();
53-
}
54-
if($(this).find('.last').hasClass('active')) {
55-
$(this).children('.right.carousel-control').hide();
56-
}
57-
});
58-
59-
$(document).ready(function(){
60-
// hide the left control when the page is ready
61-
$('.carousel').each(function() {
62-
if($(this).find('.first').hasClass('active')) {
63-
$(this).children('.left.carousel-control').hide();
64-
}
65-
if($(this).find('.last').hasClass('active')) {
66-
$(this).children('.right.carousel-control').hide();
67-
}
68-
});
69-
70-
var day = "#{@current_day}";
71-
// we only go to the date tag in the url if the conference is not taking place now
72-
if(day === ""){
73-
// use the date tag in the url to select a tab and the title of the button
74-
var hash = window.location.hash;
75-
if(hash && !(hash === '#schedule')){
76-
hash && $('ul a[href="' + hash + '"]').tab('show');
77-
$('button.dropdown-toggle').text(hash.substr(1));
78-
}
29+
$(document).ready(function() {
30+
var hash = window.location.hash;
31+
if (hash && hash !== '#schedule') {
32+
$('ul a[href="' + hash + '"]').tab('show');
7933
}
8034
});

0 commit comments

Comments
 (0)