diff --git a/css/style-mithril.css b/css/style-mithril.css index f2b123c..f86823d 100755 --- a/css/style-mithril.css +++ b/css/style-mithril.css @@ -699,4 +699,613 @@ checkbox css is modified from https://kyusuf.com/post/completely-css-custom-chec overflow: hidden; opacity: 1; z-index: 2; +} + +/** Page 2 Layout - Side by Side */ +#page2 { + display: flex; + flex-direction: column; + padding: 2em 3em 0 3em; + overflow-y: auto !important; + min-height: 100vh; +} + +.page2-content { + display: flex; + gap: 5em; + max-width: 100%; + margin: 0 auto; + width: 100%; + align-items: stretch; + flex: 1 1 auto; + margin-bottom: 2.5em; +} + +.page2-left { + flex: 0 0 550px; + min-width: 280px; + display: flex; + flex-direction: column; +} + +.page2-left .centered { + position: relative; + top: auto; + left: auto; + transform: none; + width: 100%; +} + +.page2-left .schedule-tile { + width: 100%; + padding: 1.2em; + background: rgba(255, 255, 255, 0.98); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + border: 1px solid rgba(0, 0, 0, 0.05); + flex: 1 1 auto; +} + +.page2-right { + flex: 1 1 auto; + min-width: 280px; + display: flex; + flex-direction: column; +} + +.page2-left .schedule-tile .schedule-tile-title { + margin: 0; + font-size: 1.3em; + font-weight: 600; + color: #222; + display: flex; + align-items: center; + letter-spacing: -0.5px; +} + +.page2-left .schedule-tile .schedule-tile-title i { + margin-right: 8px !important; + color: darkturquoise; + font-size: 1.2em !important; +} + +.page2-left .schedule-tile .schedule-tile-header { + margin-bottom: 0.8em; + padding-bottom: 0.8em; + border-bottom: 2px solid #f0f0f0; +} + +.page2-left .schedule-tile .schedule-tile-title { + margin: 0; + font-size: 1.3em; + font-weight: 600; + color: #222; + display: flex; + align-items: center; + letter-spacing: -0.5px; +} + +.page2-left .schedule-tile .schedule-tile-title i { + margin-right: 8px !important; + color: darkturquoise; + font-size: 1.2em !important; +} + +.page2-left .schedule-tile table { + font-size: 0.95em; + width: 100%; + margin: 0 auto; +} + +.page2-left .schedule-tile table td { + padding: 0.3em 0.5em; + text-align: left; +} + +.page2-left .schedule-tile table tr.current { + background-color: #f0fdfd; +} + +.page2-left .schedule-tile table tr.current td { + font-weight: 600; + color: #222; +} + +.page2-left .schedule-tile table tr.completed td { + color: #999; +} + +.page2-left .schedule-tile table tr.future td { + color: #666; +} + +.page2-left .schedule-tile .no-classes { + text-align: center; + padding: 1em; + color: #999; + font-size: 1em; +} + +.page2-right .todo-list-container { + max-height: none; + width: 100%; + box-sizing: border-box; +} + +@media (max-width: 900px) { + .page2-content { + flex-direction: column; + align-items: stretch; + } + + .page2-left { + flex: 1; + width: 100%; + } + + .page2-right { + width: 100%; + max-height: none; + } +} + +.page2-footer { + text-align: center; + font-size: 0.85em; + color: #999; + margin-top: auto; + padding: 1.5em 0; + border-top: 1px solid #eee; + position: relative; + flex-shrink: 0; +} + +.page2-footer .footer-right { + position: absolute; + bottom: 1.5em; + right: 3em; +} + +/** Todo List Styles */ +.todo-list-container { + width: 100%; + padding: 1.2em; + background: rgba(255, 255, 255, 0.98); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + box-sizing: border-box; + min-width: 0; + margin: 0; + border: 1px solid rgba(0, 0, 0, 0.05); + flex: 1 1 auto; +} + +.todo-list-header { + margin-bottom: 1em; + padding-bottom: 0.8em; + border-bottom: 2px solid #f0f0f0; +} + +.todo-list-title { + margin: 0; + font-size: 1.3em; + font-weight: 600; + color: #222; + display: flex; + align-items: center; + letter-spacing: -0.5px; +} + +.todo-list-title i { + margin-right: 8px !important; + color: darkturquoise; + font-size: 1.2em !important; +} + +.todo-add-form { + display: flex; + gap: 0.6em; + margin-bottom: 1em; + align-items: flex-end; + width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.todo-form-inputs { + display: flex; + gap: 0.6em; + flex: 1; + min-width: 0; + box-sizing: border-box; +} + +.todo-input { + flex: 1; + min-width: 100px; + padding: 0.7em 0.9em; + font-size: 0.9em; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-family: 'Roboto', sans-serif; + outline: none; + transition: all 0.25s ease; + background: #fafafa; + box-sizing: border-box; +} + +.todo-input::placeholder { + color: #999; + font-weight: 400; +} + +.todo-input:focus { + border-color: darkturquoise; + background: white; + box-shadow: 0 0 0 3px rgba(0, 206, 209, 0.15); + outline: none; +} + +.todo-period-select { + min-width: 100px; + padding: 0.7em 2.5em 0.7em 0.9em; + font-size: 0.85em; + border: 2px solid #e0e0e0; + border-radius: 8px; + background: #fafafa; + font-family: 'Roboto', sans-serif; + cursor: pointer; + outline: none; + transition: all 0.25s ease; + box-sizing: border-box; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.8rem center; + background-size: 1.2em; + color: #333; + font-weight: 500; +} + +.todo-period-select:focus { + border-color: darkturquoise; + background-color: white; + box-shadow: 0 0 0 3px rgba(0, 206, 209, 0.15); +} + +.todo-add-button { + padding: 0.7em 0.9em; + background: linear-gradient(135deg, darkturquoise 0%, #00b8d4 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 0.9em; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0, 206, 209, 0.35); + font-weight: 600; + flex-shrink: 0; + white-space: nowrap; +} + +.todo-add-button:hover { + background: linear-gradient(135deg, #00b8d4 0%, #00a8c8 100%); + box-shadow: 0 6px 20px rgba(0, 206, 209, 0.45); + transform: translateY(-2px); +} + +.todo-add-button:active { + background: linear-gradient(135deg, #009999 0%, #0088aa 100%); + transform: translateY(0); + box-shadow: 0 2px 6px rgba(0, 206, 209, 0.3); +} + +.todo-add-button i { + font-size: 1.1em !important; +} + +.todo-list-content { + overflow-y: auto; + overflow-x: hidden; + flex: 1; + min-height: 0; + min-width: 0; + padding-right: 0.5em; +} + +.todo-list-content::-webkit-scrollbar { + width: 8px; +} + +.todo-list-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.todo-list-content::-webkit-scrollbar-thumb { + background: #d0d0d0; + border-radius: 10px; +} + +.todo-list-content::-webkit-scrollbar-thumb:hover { + background: #aaa; +} + +.todo-group { + margin-bottom: 0.8em; + background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%); + border-radius: 10px; + padding: 0.8em; + border-left: 5px solid #ddd; + transition: all 0.3s ease; + min-width: 0; + border: 1px solid rgba(0, 0, 0, 0.06); +} + +.todo-group:hover { + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.08); + border-left-color: #999; +} + +.todo-group.current-period { + border-left-color: darkturquoise; + background: linear-gradient(135deg, #f0fdfd 0%, #e8fbfc 100%); + box-shadow: 0 4px 16px rgba(0, 206, 209, 0.25); +} + +.todo-group-header { + display: flex; + align-items: center; + gap: 0.6em; + margin-bottom: 0.6em; + font-weight: 700; + font-size: 1em; + color: #222; +} + +.todo-group-header .material-icons { + font-size: 1.1em; + color: darkturquoise; +} + +.todo-current-badge { + background: linear-gradient(135deg, darkturquoise 0%, #00b8d4 100%); + color: white; + padding: 0.35em 0.8em; + border-radius: 16px; + font-size: 0.8em; + font-weight: 700; + margin-left: auto; + box-shadow: 0 2px 6px rgba(0, 206, 209, 0.3); +} + +.todo-items { + display: flex; + flex-direction: column; + gap: 0.4em; + min-width: 0; +} + +.todo-item { + display: flex; + align-items: center; + gap: 0.7em; + padding: 0.7em 0.9em; + background: white; + border-radius: 8px; + border: 1px solid #e8e8e8; + transition: all 0.25s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + min-width: 0; +} + +.todo-item:last-child { + margin-bottom: 0; +} + +.todo-item:hover { + border-color: #d0d0d0; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); + transform: translateX(3px); +} + +.todo-checkbox { + display: none; +} + +.todo-checkbox:hover { + transform: scale(1.1); +} + +@keyframes pop-scale { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } +} + +.todo-complete { + width: 4em; + height: 4em; + cursor: pointer; + flex-shrink: 0; + background: transparent; + border: none; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + color: #d0d0d0; +} + +.todo-complete:hover { + color: darkturquoise; + animation: pop-scale 0.4s ease-in-out; +} + +.todo-complete:active { + transform: scale(0.9); +} + +.todo-complete .material-icons { + font-size: 4em; +} + +.todo-text { + flex: 1; + font-size: 0.9em; + color: #333; + word-break: break-word; + font-weight: 500; + min-width: 0; + overflow-wrap: break-word; +} + +.todo-delete { + display: none; +} + +.todo-delete:hover { + background: #ffe8e8; + color: #ff4444; + transform: scale(1.1); +} + +.todo-delete:active { + transform: scale(0.95); +} + +.todo-delete .material-icons { + font-size: 1.3em; +} + +.todo-empty { + text-align: center; + padding: 2em 2em; + color: #bbb; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + background: linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%); + border-radius: 10px; + border: 2px dashed #e8ecf0; + margin: 0; + min-height: 400px; +} + +@keyframes float-bounce { + 0%, 100% { + transform: translateY(0px) scale(1); + } + 50% { + transform: translateY(-15px) scale(1.05); + } +} + +.todo-empty p { + font-size: 1.3em; + margin: 0; + font-weight: 500; + color: #666; + letter-spacing: 0.3px; + text-align: center; +} + +@media (max-width: 768px) { + .todo-list-container { + padding: 1.5em; + max-height: calc(100vh - 4em); + } + + .todo-list-title { + font-size: 1.6em; + } + + .todo-add-form { + flex-direction: column; + gap: 0.8em; + align-items: stretch; + } + + .todo-form-inputs { + flex-direction: column; + width: 100%; + } + + .todo-input, + .todo-period-select, + .todo-add-button { + width: 100%; + } + + .todo-add-button { + padding: 1em; + } + + .todo-item { + padding: 1em; + gap: 0.8em; + } + + .todo-delete .material-icons { + font-size: 1.4em; + } +} + +@media (max-width: 480px) { + .todo-list-container { + padding: 1em; + max-height: calc(100vh - 3em); + border-radius: 8px; + } + + .todo-list-title { + font-size: 1.4em; + } + + .todo-list-header { + margin-bottom: 1.2em; + padding-bottom: 1em; + } + + .todo-add-form { + margin-bottom: 1.5em; + } + + .todo-group { + margin-bottom: 1em; + padding: 1em; + } + + .todo-item { + padding: 0.9em; + } + + .todo-text { + font-size: 1em; + } + + #page2 { + padding: 1em 0.5em; + } + + .page2-content { + gap: 1em; + } } \ No newline at end of file diff --git a/css/style.css b/css/style.css index 40ffae0..c7b087d 100755 --- a/css/style.css +++ b/css/style.css @@ -2,12 +2,13 @@ body { font-family: 'Roboto', sans-serif; transition: background-color 3s ease; margin: 0; - // cursor: default; - // -webkit-touch-callout: none; - // -webkit-user-select: none; - // -moz-user-select: none; - // user-select: none; - // overflow-x: hidden; + /* cursor: default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + overflow-x: hidden; */ + /* fixed these comments-- they were using html comment syntax which breaks CSS :) */ } .selectize-dropdown, .selectize-input { @@ -158,7 +159,7 @@ a.add:hover { background-color: #e6e6e6; } .article-body { - // padding: 0px; + /* padding: 0px; */ margin-bottom: 80px; } .article-body blockquote { diff --git a/src/ui/Index.js b/src/ui/Index.js index 0071147..b634337 100644 --- a/src/ui/Index.js +++ b/src/ui/Index.js @@ -1,6 +1,7 @@ const m = require('mithril') const ScheduleDisplay = require('./ScheduleDisplay') +const TodoList = require('./TodoList') const Page1 = require('./Page1') const Popup = require('./Popup') const SchoolIndicator = require('./SchoolIndicator') @@ -81,7 +82,15 @@ const Index = { }, [ m(Page1, vnode.attrs), m('.container#page2', [ - m(ScheduleDisplay, vnode.attrs), + m('.page2-content', [ + m('.page2-left', [ + m(ScheduleDisplay, vnode.attrs) + ]), + m('.page2-right', [ + m(TodoList, vnode.attrs) + ]) + ]), + m('.page2-footer', 'Saved by the bell.plus!'), m(SettingsIcon) ]) ]), diff --git a/src/ui/ScheduleDisplay.js b/src/ui/ScheduleDisplay.js index 47cf0ee..2f3c270 100644 --- a/src/ui/ScheduleDisplay.js +++ b/src/ui/ScheduleDisplay.js @@ -34,7 +34,15 @@ var ScheduleDisplay = { future = future.slice(0, numberOfFuturePeriods) if (!completed.length && !future.length && current.name === 'Free') { - return m('.centered', m('.no-classes', 'No classes today')) + return m('.schedule-tile', [ + m('.schedule-tile-header', + m('.schedule-tile-title', [ + m('i.material-icons', 'schedule'), + 'Schedule' + ]) + ), + m('.centered', m('.no-classes', 'No classes today')) + ]) } var rows = [] @@ -47,7 +55,15 @@ var ScheduleDisplay = { } for (let period of future) { rows.push(m('tr.future', [m('td.time', displayTimeArray(period.time)), m('td', period.name)])) } - return m('.centered', m('table', rows)) + return m('.schedule-tile', [ + m('.schedule-tile-header', + m('.schedule-tile-title', [ + m('i.material-icons', 'schedule'), + 'Schedule' + ]) + ), + m('.centered', m('table', rows)) + ]) } } diff --git a/src/ui/TodoList.js b/src/ui/TodoList.js new file mode 100644 index 0000000..0aa2bba --- /dev/null +++ b/src/ui/TodoList.js @@ -0,0 +1,206 @@ +const m = require('mithril') +const shortid = require('shortid') + +const TodoList = { + oninit: function (vnode) { + vnode.state.newTaskText = '' + vnode.state.selectedPeriod = '' + // Filter out completed tasks on init (for backwards compatibility) + const allTodos = vnode.attrs.cookieManager.get('todos', []) + vnode.state.todos = allTodos.filter(t => t.completed !== true) + // Save the filtered list back if any were removed + if (allTodos.length !== vnode.state.todos.length) { + vnode.attrs.cookieManager.set('todos', vnode.state.todos) + } + }, + + getAvailablePeriods: function (bellTimer) { + const periods = [] + const completed = bellTimer.getCompletedPeriods() + const current = bellTimer.getCurrentPeriod() + const future = bellTimer.getFuturePeriods() + + // Add all periods, excluding Free and any period with "Passing" in the name + completed.forEach(p => { + if (p.name !== 'Free' && + !p.name.includes('Passing') && + !periods.find(ep => ep.name === p.name)) { + periods.push(p) + } + }) + if (current && + current.name !== 'Free' && + !current.name.includes('Passing') && + !periods.find(ep => ep.name === current.name)) { + periods.push(current) + } + future.forEach(p => { + if (p.name !== 'Free' && + !p.name.includes('Passing') && + !periods.find(ep => ep.name === p.name)) { + periods.push(p) + } + }) + + // Sort by time + periods.sort((a, b) => { + const timeA = a.time.hour * 60 + a.time.min + const timeB = b.time.hour * 60 + b.time.min + return timeA - timeB + }) + + return periods + }, + + addTodo: async function (vnode) { + const text = vnode.state.newTaskText.trim() + if (!text) return + + const newTodo = { + id: shortid.generate(), + text: text, + period: vnode.state.selectedPeriod || null, + createdAt: Date.now() + } + + vnode.state.todos.push(newTodo) + vnode.state.newTaskText = '' + vnode.state.selectedPeriod = '' + await vnode.attrs.cookieManager.set('todos', vnode.state.todos) + m.redraw() + }, + + toggleTodo: async function (vnode, todoId) { + // When a todo is checked, delete it instead of marking as completed + vnode.state.todos = vnode.state.todos.filter(t => t.id !== todoId) + await vnode.attrs.cookieManager.set('todos', vnode.state.todos) + m.redraw() + }, + + deleteTodo: async function (vnode, todoId) { + vnode.state.todos = vnode.state.todos.filter(t => t.id !== todoId) + await vnode.attrs.cookieManager.set('todos', vnode.state.todos) + m.redraw() + }, + + getTodosForPeriod: function (todos, periodName) { + if (!periodName) { + return todos.filter(t => !t.period) + } + return todos.filter(t => t.period === periodName) + }, + + view: function (vnode) { + const bellTimer = vnode.attrs.bellTimer + const cookieManager = vnode.attrs.cookieManager + const todos = vnode.state.todos || [] + const availablePeriods = this.getAvailablePeriods(bellTimer) + const currentPeriod = bellTimer.getCurrentPeriod() + + // Group todos by period (only show incomplete todos) + const todosByPeriod = {} + const unassignedTodos = [] + + todos.forEach(todo => { + if (todo.period) { + if (!todosByPeriod[todo.period]) { + todosByPeriod[todo.period] = [] + } + todosByPeriod[todo.period].push(todo) + } else { + unassignedTodos.push(todo) + } + }) + + return m('.todo-list-container', [ + m('.todo-list-header', [ + m('h2.todo-list-title', [ + m('i.material-icons', { style: { 'vertical-align': 'middle', 'margin-right': '8px' } }, 'check_circle'), + 'To-Do List' + ]) + ]), + + // Add new todo form + m('.todo-add-form', [ + m('.todo-form-inputs', [ + m('input.todo-input[type=text][placeholder=Add a task...]', { + value: vnode.state.newTaskText, + oninput: m.withAttr('value', function (value) { + vnode.state.newTaskText = value + }), + onkeypress: function (e) { + if (e.key === 'Enter') { + TodoList.addTodo(vnode) + } + } + }), + m('select.todo-period-select', { + value: vnode.state.selectedPeriod, + onchange: m.withAttr('value', function (value) { + vnode.state.selectedPeriod = value + }) + }, [ + m('option[value=]', 'General'), + ...availablePeriods.map(p => m('option[value=' + p.name + ']', p.name)) + ]) + ]), + m('button.todo-add-button', { + onclick: function () { + TodoList.addTodo(vnode) + } + }, [ + m('i.material-icons', 'add') + ]) + ]), + + // Display todos + m('.todo-list-content', [ + // Unassigned todos + unassignedTodos.length > 0 && m('.todo-group', [ + m('.todo-group-header', [ + m('i.material-icons', 'list'), + m('span', 'General') + ]), + m('.todo-items', unassignedTodos.map(todo => m('.todo-item', [ + m('button.todo-complete', { + onclick: function () { + TodoList.toggleTodo(vnode, todo.id) + } + }, m('i.material-icons', 'check_circle')), + m('span.todo-text', todo.text) + ]))) + ]), + + // Todos by period + Object.keys(todosByPeriod).sort().map(periodName => { + const isCurrentPeriod = currentPeriod && currentPeriod.name === periodName + return m('.todo-group', { + class: isCurrentPeriod ? 'current-period' : '' + }, [ + m('.todo-group-header', [ + m('i.material-icons', isCurrentPeriod ? 'schedule' : 'class'), + m('span', periodName), + isCurrentPeriod && m('span.todo-current-badge', 'Current') + ]), + m('.todo-items', todosByPeriod[periodName].map(todo => m('.todo-item', [ + m('button.todo-complete', { + onclick: function () { + TodoList.toggleTodo(vnode, todo.id) + } + }, m('i.material-icons', 'check_circle')), + m('span.todo-text', todo.text) + ]))) + ]) + }), + + // Empty state + todos.length === 0 && m('.todo-empty', [ + + m('p', 'Looks like you\'re all caught up! 🌴'), + ]) + ]) + ]) + } +} + +module.exports = TodoList