From e3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 22 Dec 2024 21:53:57 +0100 Subject: Rework of halfnarp and fullnarp into a self contained repository. Still WIP --- static/halfnarp.js | 510 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 static/halfnarp.js (limited to 'static/halfnarp.js') diff --git a/static/halfnarp.js b/static/halfnarp.js new file mode 100644 index 0000000..878f3ab --- /dev/null +++ b/static/halfnarp.js @@ -0,0 +1,510 @@ +function toggle_grid(whichDay) { + var vclasses= [['in-list'], ['in-calendar', 'onlyday1'], ['in-calendar', 'onlyday2'], ['in-calendar', 'onlyday3'], + ['in-calendar', 'onlyday4'], ['in-calendar', 'alldays']]; + document.body.classList.remove( 'alldays', 'onlyday1', 'onlyday2', 'onlyday3', 'onlyday4', 'in-list', 'in-calendar'); + if( whichDay < 0 || whichDay > 5 ) return; + document.body.classList.add(...vclasses[whichDay]); + document.getElementById('qrcode').classList.toggle('limit', whichDay == 0); +} + +function toggle_corr_mode() { + if (!document.body.classList.contains('correlate')) + document.body.classList.remove('all-tracks', 'languages', 'classifiers'); + document.querySelectorAll('.event').forEach(elem => elem.setAttribute('corr', '')); + } + document.body.classList.toggle('correlate'); +} + +function toggle_classifier(classifier, is_track, is_range) { + if (document.body.classList.contains('classifiers') && document.body.getAttribute('classifiers') == classifier) { + document.body.classList.remove('classifiers'); + return; + } + var default_intensity = 0, prefix = ''; + if (is_range) { + default_intensity = 5; + prefix = '+'; + } + is_range = is_range ? '+' : ''; + for (ev of window.top.all_events) { + if (ev.event_classifiers) { + var intensity = default_intensity; + // if track selector and empty, set to 80% + if (ev.event_classifiers[classifier]) + intensity = Math.round(ev.event_classifiers[classifier] / 10); + $('#event_'+ev.event_id).attr('intensity', prefix + intensity); + } + } + document.body.classList.add('classifiers'); + document.body.classList.remove('all-tracks', 'languages', 'correlate'); + document.body.setAttribute('classifier', classifier); +} + +function set_random_event() { + var keys = Object.keys(all_events).filter(function(event_id) { + return $('#event_'+event_id+'.selected').length == 0 && + $('#event_'+event_id+'.rejected').length == 0; + }); + + if (keys.length == 0) { + $('.narpr').toggleClass('hidden'); + return; + } + + item = all_events[keys[ keys.length * Math.random() << 0]]; + $('.narpr_title').text(item.title || ''); + $('.narpr_track').text(item.track_name || ''); + $('.narpr_subtitle').text(item.subtitle || ''); + $('.narpr_speakers').text(item.speaker_names || ''); + $('.narpr_abstract').html(item.abstract || ''); + window.narpr_event = item.event_id; +} + +function redraw_qrcode(ids) { + if (!ids) + ids = $('.selected').map( function() { return $(this).attr('event_id'); }).get(); + if ($('#qrcode').hasClass('hidden') && ids.length == 0 ) + return; + var request = JSON.stringify({'talk_ids': ids}); + var size = 68; + if($('body').hasClass('qrcode-huge')) { + size = 400; + } + + $('#qrcode').empty(); + $('#qrcode').qrcode({width: size, height: size, text: request}); + $('#qrcode').removeClass('hidden'); +} + +function redraw_calendar(myuid, ids) { + if (!ids) + ids = $('.selected').map( function() { return $(this).attr('event_id'); }).get(); + + var now = new Date(); + var calendar = 'BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//events.ccc.de//halfnarp//EN\r\nX-WR-TIMEZONE:Europe/Berlin\r\n'; + ids.forEach( function(id) { + var item = all_events[id]; + if ('start_time' in item) { + + var start = new Date(item.start_time); + calendar += 'BEGIN:VEVENT\r\n'; + calendar += 'UID:'+myuid+item.event_id+'\r\n'; + calendar += 'DTSTAMP:' + now.toISOString().replace(/-|;|:|\./g, '').replace(/...Z$/, 'Z') + '\r\n'; + calendar += 'DTSTART:' + start.toISOString().replace(/-|;|:|\./g, '').replace(/...Z$/, 'Z') + '\r\n'; + calendar += 'DURATION:PT' + item.duration + 'S\r\n'; + calendar += 'LOCATION:' + item.room_name + '\r\n'; + calendar += 'URL:http://events.ccc.de/congress/2023/Fahrplan/events/' + item.event_id + '.html\r\n'; + calendar += 'SUMMARY:' + item.title + '\r\n'; + calendar += 'DESCRIPTION:' + item.abstract.replace(/\n|\r/g, ' ') + '\r\n'; + // console.log( 'id:' + id + ' ' + all_events[id] ); + // console.log( all_events[id].title ); + calendar += 'END:VEVENT\r\n'; + } + }); + calendar += 'END:VCALENDAR\r\n'; + $('.export-url-a').attr( 'href', "data:text/calendar;filename=38C3.ics," + encodeURIComponent(calendar) ); + $('.export-url').removeClass( 'hidden' ); +} + +function do_the_halfnarp() { +// var halfnarpAPI = 'talks_36C3.json'; + var halfnarpAPI = '/-/talkpreferences'; + var halfnarpCorrs = 'corr_array_38c3.json'; + var halfnarpPubAPI = halfnarpAPI + '/public/'; + var isTouch = (('ontouchstart' in window) || (navigator.msMaxTouchPoints > 0)); + window.top.all_events = new Object(); + window.top.narpr_rejected = new Array(); + var myuid, mypid, newfriend = new Object(); + var allhours = ['10', '11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02','03']; + + $('.narpr_done').click( function(ev) { + $('.narpr').toggleClass('hidden', true); + if (!window.narpr_alerted) { + window.narpr_alerted = true; + alert("Thank you for using narpr(β). Don't forget to SUBMIT!"); + } + }); + + $('.narpr').on({ 'touchstart' : function(ev) { + // alert("foo: " + ev.originalEvent.touches[0].clientX ); + window.touch_startX = ev.originalEvent.touches[0].clientX; + window.touch_curX = ev.originalEvent.touches[0].clientX; + window.touch_valid = true; + $('.narpr').css('background', '#ddd'); + } }); + + $('.narpr').on({ 'touchmove' : function(ev) { + var narp_view = $('.narpr'); + if (ev.originalEvent.touches.length > 1) { + narp_view.css('background', 'white'); + window.touch_valid = false; + } + if (!window.touch_valid) + return; + if (ev.originalEvent.touches[0].clientX > window.touch_startX + 100) + narp_view.css('background', 'green'); + else if (ev.originalEvent.touches[0].clientX < window.touch_startX - 100) + narp_view.css('background', 'red'); + else + narp_view.css('background', '#ddd'); + window.touch_curX = ev.originalEvent.touches[0].clientX; + + // console.log(narp_view[0].clientHeight + ':' + narp_view[0].scrollHeight); + if( narp_view[0].clientHeight >= narp_view[0].scrollHeight) + ev.preventDefault(); + } }); + + $('.narpr').on({ 'touchend' : function(ev) { + if (!window.touch_valid) + return; + if (window.touch_curX > window.touch_startX + 100) { + $('#event_'+window.narpr_event).toggleClass('selected', true); + set_random_event(); + } + if (window.touch_curX < window.touch_startX - 100) { + $('#event_'+window.narpr_event).toggleClass('selected', false); + $('#event_'+window.narpr_event).toggleClass('rejected', true); + set_random_event(); + } + + $('.narpr').css('background', 'white'); + } }); + + /* Add callback for submit click */ + $('.submit').click( function() { + var myapi; + + /* Get user's preferences and try to save them locally */ + var ids = $('.selected').map( function() { + return $(this).attr('event_id'); + }).get(); + try { + localStorage['38C3-halfnarp'] = ids; + myapi = localStorage.getItem('38C3-halfnarp-api'); + if (myapi) { + myapi = myapi.replace(/.*?:\//g, ""); + myapi = 'https:/' + myapi.replace(/.*?:\//g, ""); + } + } catch(err) { + alert('Storing your choices locally is forbidden.'); + } + + /* Convert preferences to JSON and post them to backend */ + var request = JSON.stringify({'talk_ids': ids}); + if( !myapi || !myapi.length ) { + /* If we do not have resource URL, post data and get resource */ + $.ajax({ + type: 'POST', + url: halfnarpAPI + '/', + data: request, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + contentType: "text/plain", + dataType: 'json', + }).done(function(data) { + $('.info').text('submitted'); + $('.info').removeClass('hidden'); + try { + localStorage['38C3-halfnarp-api'] = data['update_url']; + localStorage['38C3-halfnarp-pid'] = mypid = data['hashed_uid']; + localStorage['38C3-halfnarp-uid'] = myuid = data['uid']; + window.location.hash = mypid; + } catch(err) {} + }, 'json' ).fail(function() { + $('.info').text('failed :('); + $('.info').removeClass('hidden'); + }); + } else { + /* If we do have a resource URL, update resource */ + $.ajax({ + type: 'PUT', + url: myapi, + data: request, + contentType: "application/json", + dataType: 'json', + }).done(function(data) { + localStorage['38C3-halfnarp-uid'] = myuid = data['uid']; + if( localStorage['38C3-halfnarp-pid'] ) { + window.location.hash = localStorage['38C3-halfnarp-pid']; + } + $('.info').text('updated'); + $('.info').removeClass('hidden'); + }).fail(function(msg) { + $('.info').text('failed :('); + $('.info').removeClass('hidden'); + }); + } + + /* Tell QRCode library to update and/or display preferences for Apps */ + redraw_qrcode(ids); + + if (myuid) + redraw_calendar(myuid, ids); + }); + + /* Add handler for type ahead search input field */ + var filter = document.getElementById('filter'); + filter.onpaste = filter.oncut = filter.onkeypress = filter.onkeydown = filter.onkeyup = function() { + var cnt = this.value.toLowerCase(); + if( cnt.length ) + document.querySelectorAll('.event').forEach(elem => elem.style.display = (elem.textContent || elem.innerText || '').toLowerCase().includes(cnt) ? "initial" : "none" ); + else + document.querySelectorAll('.event').forEach(elem => elem.style.display = "initial"); + }; + + /* Add click handlers for event div sizers */ + document.querySelector('.vsmallboxes').onclick = function() { + document.body.classList.remove('size-medium', 'size-large'); + document.body.classList.add('size-small'); + }; + + document.querySelector('.vmediumboxes').onclick = function() { + document.body.classList.remove('size-small', 'size-large'); + document.body.classList.add('size-medium'); + }; + + document.querySelector('.vlargeboxes').onclick = function() { + document.body.classList.remove('size-small', 'size-medium'); + document.body.classList.add('size-large'); + }; + + /* Add de-highlighter on touch interface devices */ + if( isTouch ) { + document.body.onclick = function() { + document.querySelector('.highlighted').forEach(elem => elem.classList.remove('highlighted')); + }; + document.querySelector('.touch-only').forEach(elem => elem.classList.remove('hidden')); + } + + /* Add callbacks for view selector */ + document.querySelector('.vlist').onclick = function() { toggle_grid(0); }; + document.querySelector('.vday1').onclick = function() { toggle_grid(1); }; + document.querySelector('.vday2').onclick = function() { toggle_grid(2); }; + document.querySelector('.vday3').onclick = function() { toggle_grid(3); }; + document.querySelector('.vday4').onclick = function() { toggle_grid(4); }; + document.querySelector('.vdays').onclick = function() { toggle_grid(5); }; + + document.querySelector('.vlang').onclick = function() { document.body.classList.toggle('languages'); }; + document.querySelector('.vtrack').onclick = function() { document.body.classList.toggle('all-tracks'); }; + document.querySelector('.vnarpr').onclick = function() { $('.narpr').toggleClass('hidden'); set_random_event(); }; + + document.querySelector('.vcorr').onclick = toggle_corr_mode; + + $('.vclass').click( function() { toggle_classifier( $(this).attr('classifier'), $(this).hasClass('track'), $(this).hasClass('two_poles')); }); + + /* Create hour guides */ + for (hour of allhours) { + var elem = document.createElement('hr'); + elem.classList.add('guide', 'time_' + hour + '00'); + document.body.append(elem); + elem = document.createElement('div'); + elem.textContent = hour + '00'; + elem.classList.add('guide', 'time_' + hour + '00'); + document.body.append(elem); + } + + /* If we've been here before, try to get local preferences. They are authoratative */ + var selection = [], friends = { }; + try { + selection = localStorage['38C3-halfnarp'] || []; + friends = localStorage['38C3-halfnarp-friends'] || { }; + myuid = localStorage['38C3-halfnarp-uid'] || ''; + mypid = localStorage['38C3-halfnarp-pid'] || ''; + } catch(err) { + } + + /* Fetch list of lectures to display */ + $.getJSON( halfnarpAPI, { format: 'json' }) + .done(function( data ) { + $.each( data, function( i, item ) { + /* Save event to all_events hash */ + all_events[item.event_id] = item; + + /* Take copy of hidden event template div and select them, if they're in + list of previous prereferences */ + var t = $( '#template' ).clone(true); + var event_id = item.event_id.toString(); + t.addClass('event ' + ' lang_' + (item.language || 'en')); + t.attr('event_id', item.event_id.toString()); + t.attr('id', 'event_' + item.event_id.toString()); + if( selection && selection.indexOf(item.event_id) != -1 ) { + t.addClass( 'selected' ); + } + + /* Sort textual info into event div */ + t.find('.title').text(item.title); + t.find('.speakers').text(item.speaker_names); + t.find('.abstract').append(item.abstract); + + if (item.event_classifiers && item.event_classifiers['Foundations'] && item.event_classifiers['Foundations'] > 40.0) + t.addClass('foundation'); + + /* start_time: 2014-12-29T21:15:00+01:00" */ + var start_time = new Date(item.start_time); + + var day = start_time.getUTCDate() - 26; + var hour = start_time.getUTCHours() + 1; + var mins = start_time.getUTCMinutes(); + + /* After midnight: sort into yesterday */ + if( hour < 9 ) + day--; + if( hour > 23) + hour -= 24; + + /* Fix up room for 38C3 */ + room = (item.room_id || '').toString().replace('1','room1').replace('2','room2').replace('3','room3'); + + /* Apply attributes to sort events into calendar */ + t.addClass(room + ' duration_' + item.duration + ' day_'+day + ' time_' + (hour<10?'0':'') + hour + '' + (mins<10?'0':'') + mins); + + t.click( function(event) { + if ($('body').hasClass('correlate')) { + mark_corr($(this).attr('event_id')); + event.stopPropagation(); + return; + } + /* Transition for touch devices is highlighted => selected => highlighted ... */ + if( isTouch ) { + if ( $( this ).hasClass('highlighted') ) { + $( this ).toggleClass('selected'); + $('.info').addClass('hidden'); + } else { + $('.highlighted').removeClass('highlighted'); + $( this ).addClass('highlighted'); + } + } else { + $( this ).toggleClass('selected'); + $('.info').addClass('hidden'); + } + event.stopPropagation(); + }); + /* Put new event into DOM tree. Track defaults to 'Other' */ + try { + var track = item.track_id.toString(); + } catch(e) { + var track = "Other"; + } + var d = $( '#' + track ); + t.addClass('track_' + track ); + if( !d.length ) { + d = $( '#Other' ); + } + d.append(t); + if( newfriend.pid ) { + newfriend.prefs.forEach( function( eventid ) { + $( '#event_' + eventid ).addClass( 'friend' ); + }); + } + }); + + $.getJSON( halfnarpCorrs, { format: 'json' }).done(function(data) { window.top.all_votes = data; }); + toggle_grid(5); + + /* Check for a new friends public uid in location's #hash */ + var shared = window.location.hash; + shared = shared ? shared.substr(1) : ''; + if( shared.length ) { + if ( ( friends[shared] ) || ( shared === mypid ) ) { + + } else { + $.getJSON( halfnarpPubAPI + shared, { format: 'json' }) + .done(function( data ) { + newfriend.pid = shared; + newfriend.prefs = data.talk_ids; + newfriend.prefs.forEach( function( eventid ) { + $( '#event_' + eventid ).addClass( 'friend' ); + }); + }); + } + } + // window.location.hash = ''; + + ids = $('.selected').map( function() { return $(this).attr('event_id'); }).get(); + if (ids.length) { + redraw_qrcode(ids); + if (myuid) + redraw_calendar(myuid, ids); + } + + $('#qrcode').click( function() { + $('body').toggleClass('qrcode-huge'); + redraw_qrcode(); + }); + + /* Update friends cache */ + for( var friend in friends ) { + $.getJSON( halfnarpPubAPI + friends.pid, { format: 'json' }) + .done(function( data ) { + friend.prefs = data.talk_ids; + localStorage['38C3-halfnarp-friends'] = friends; + update_friends(); + }); + } + + }); + document.onkeypress = function(e) { + if( document.activeElement.tagName == 'INPUT' || document.activeElement.tagName == 'TEXTAREA' ) + return; + switch( e.keyCode ) { + case 48: case 94: /* 0 */ + toggle_grid(5); + break; + case 49: case 50: case 51: case 52: /* 1-4 */ +/* toggle_grid(e.keyCode-48); */ + break; + case 76: case 108: /* l */ + toggle_grid(0); + break; + case 68: case 100: /* d */ +/* toggle_grid(5); */ + break; + case 73: case 105: /* i */ + document.body.classList.remove('all-tracks'); + document.body.classList.toggle('languages'); + break; + case 84: case 116: /* t */ + document.body.classList.remove('languages'); + document.body.classList.toggle('all-tracks'); + break; + case 67: case 99: /* c */ +/* toggle_corr_mode(); */ + break; + } + }; +} + +function mark_corr(eid) { + /* If JSON with votes is not there, bail */ + if (!all_votes) return; + + /* Reset correlation markers */ + document.querySelectorAll('.event').forEach(elem => elem.setAttribute('corr', ''); + + /* Get index of reference event id */ + var eoff = all_votes.event_ids.indexOf(eid); + if (eoff==-1) return; + + document.querySelectorAll('.event').forEach( function(dest) { + var destid = dest.getAttribute('event_id'); + /* mark reference event at another place */ + if (destid == eid) { + dest.setAttribute('corr', 'x'); + return; + } + + var destoff = all_votes.event_ids.indexOf(destid); + if (destoff==-1) { + dest.setAttribute('corr', '0'); + return; + } + + /* Only the smaller event-id's string has the info */ + if (eoff < destoff) + dest.setAttribute('corr', all_votes.event_corrs[eoff].charAt(destoff-eoff-1)); + else + dest.setAttribute('corr', all_votes.event_corrs[destoff].charAt(eoff-destoff-1)); + }); +} -- cgit v1.2.3