<!--
TO ADD SOMETIME:
- 'remove item' button - maybe with swipe functionality for mobile
- standardize colors + tidy/tighten CSS
- smarter js???
- display currently selected conditions in list view
- refine and improve the list itself - for example I probably won't want to bring my uke if it's raining.
- SVG hikers/bikers/campers/etc hanging out in background as the conditions are set
- bg of custom item field is clipped to show page bg ..?
- add 'copy text' notification to plaintext box
-->
<!-- start page -->
<div id="page-start">
<header>
<p>A project for fun and utility</p>
<p>Concept, development, design, and assets by Erin Knowles</p>
<p>September 2019</p>
<h1>Camping packing list generator</h1>
</header>
<input type="button" value="Get started" id="button-start" class="button">
</div>
<!-- start page 1: people -->
<div id="page1">
<header>
<p>Question 1/4</p>
<h2>How many people are coming?</h2>
</header>
<div id="people-wrap">
<input type="number" value="1" id="people">
<div class="number-input">
<input type="button" value="^" id="people-plus">
<input type="button" value="^" id="people-minus">
</div>
</div>
<input type="button" value="Next" id="button-page1" class="button button-light">
</div>
<!-- start page 2: nights -->
<div id="page2">
<header>
<p>Question 2/4</p>
<h2>How many nights will you be gone?
<span>(0 for a day trip)</span>
</h2>
</header>
<div id="nights-wrap">
<input type="number" value="0" id="nights">
<div class="number-input">
<input type="button" value="^" id="nights-plus">
<input type="button" value="^" id="nights-minus">
</div>
</div>
<input type="button" value="Next" id="button-page2" class="button button-light">
</div>
<!-- start page 3: biking -->
<div id="page3">
<header>
<p>Question 3/4</p>
<h2>Will you be biking there?</h2>
</header>
<div id="biking-wrap">
<input type="checkbox" value="biking" id="biking">
<label for="biking">
<div class="toggle-item" id="biking-yes">Yes</div>
<div class="toggle-item toggle-on" id="biking-no">No</div>
</label>
</div>
<input type="button" value="Next" id="button-page3" class="button button-light">
</div>
</div>
<!-- start page 4: rain -->
<div id="page4">
<header>
<p>Question 4/4</p>
<h2>Are you expecting rain?
<span>(Trips longer than a few days should expect rain, regardless of the forecast!)</span>
</h2>
</header>
<div id="rain-wrap">
<input type="checkbox" value="rain" id="rain">
<label for="rain">
<div class="toggle-item" id="rain-yes">Yes</div>
<div class="toggle-item toggle-on" id="rain-no">No</div>
</label>
</div>
<input type="button" value="Generate!" id="button-generate" class="button">
</div>
<!-- start to-do list: -->
<section id="page-list">
<header>
<p class="button-wrap">
<input type="button" id="list-print-list" class="button button-light button-startover" value="(get plaintext)">
<input type="button" id="button-startover" class="button button-light button-startover" value="(start over)">
</p>
<h1>My list:</h1>
</header>
<div id="custom-items" class="wrap">
<p class="list-item-label">My custom items</p>
<p class="value"></p>
</div>
<form><div id="custom-item-field-wrap">
<p>Add custom item:</p>
<input type="text" id="custom-item-value" autocomplete="off">
<input type="submit" value="Add" id="custom-item-add">
</div></form>
<div id="print-list">
<p onclick="$('#print-list').hide(200)">Close</p>
<textarea></textarea>
<div id="copy-confirm">Text copied!</div>
</div>
</section>
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0;}
html, body {
min-height: 100%;
font-size: 2vw;
}
body {
font-family: 'open sans', sans-serif;
background: url("https://i.postimg.cc/BZBDrcNg/bg2-wide-01.png");
background-position: center;
background-attachment: fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
padding: 5rem;
}
header {
margin-top: 5rem;
}
header p {
font-size: 2rem;
color: #666c7c;
opacity: 0.7;
text-align: center;
padding: 1rem 0;
}
header h1 {
text-align: center;
font-size: 4rem;
font-weight: bold;
color: #444C62;
margin: 3rem 0 6rem 0;
}
header h2 {
text-align: center;
font-size: 3rem;
font-weight: bold;
color: #444C62;
margin: 0.5rem 0 3rem 0;
}
header h2 span {
display: block;
font-size: 2rem;
opacity: 0.6;
font-weight: 500;
margin-top: 0.5rem;
}
.button {
display: block;
margin: 0 auto;
background: #1111FF;
border: 0;
border-radius: 1rem;
padding: 1.5rem 2.5rem;
color: white;
font-family: 'open sans', sans-serif;
font-size: 2rem;
font-weight: bold;
box-shadow: 0 0.5rem 1.5rem rgba(0,0,80,0.4);
}
.button-light {
background: transparent;
box-shadow: inset 0 0.5rem 1.5rem rgba(255,255,255,0.2), 0 0.5rem 1.5rem rgba(0,0,80,0.3);
opacity: 0.7;
color: #000066;
}
.button-startover {
font-size: 1.4rem;
opacity: 0.5;
padding: 1rem 1.5rem;
margin: 0 2rem 2rem 2rem;
border: 0;
}
#page-start {
display: block;
}
#page1, #page2, #page3, #page4 {
display: none;
}
#people-wrap, #nights-wrap {
display: flex;
width: 100%;
margin: 5.5rem auto 5rem auto;
justify-content: center;
}
input[type="number"] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
width: 40%;
display: inline-block;
height: auto;
font-size: 7.5rem;
padding: 1rem 1.5rem;
color: #444C62;
border: 0;
background: none;
background: -moz-linear-gradient(top, rgba(63,90,86,0) 30%, rgba(53,77,77,0.3) 100%);
background: -webkit-gradient(left top, left bottom, color-stop(30%, rgba(63,90,86,0)), color-stop(100%, rgba(53,77,77,0.3)));
background: -webkit-linear-gradient(top, rgba(63,90,86,0) 30%, rgba(53,77,77,0.3) 100%);
}
.number-imput {
display: flex;
flex-direction: column;
}
.number-input input[type="button"] {
background: none;
width: 100%;
color: #444C62;
font-size: 7.5rem;
line-height: 4.75rem;
border: 0;
}
.number-input input[type="button"]:last-of-type {
transform: rotate(180deg);
}
label {
width: 100%;
display: flex;
justify-content: center;
margin: 0 auto;
}
#biking-wrap label, #rain-wrap label {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
margin: 0.5rem 0 5rem 0;
}
label span {
font-size: 0.6rem;
color: #666;
}
input[type="checkbox"] {
display: none;
}
.toggle-item {
font-size: 7.5rem;
color: #444C62;
font-weight: bold;
padding: 1rem 2rem;
opacity: 0.5;
}
.toggle-on {
opacity: 1;
}
#page-list {
display: none;
width: 100%;
box-sizing: border-box;
padding: 0;
margin: 0;
margin-top: -5rem;
margin-bottom: 7rem;
}
#page-list h1 {
margin-bottom: 4rem;
margin-top: 0;
font-size: 2.5rem;
}
#page-list header p {
margin-bottom: 2rem;
display: flex;
justify-content: center;
}
.wrap {
padding: 2rem;
background: white;
border-radius: 1rem;
margin-bottom: 2rem;
font-weight: 600;
width: 100%;
cursor: default;
box-shadow: 0 0.5rem 1rem rgba(30,50,90,0.1);
}
.list-item-label {
margin-bottom: 0.5rem;
color: rgba(0,0,0,0.6);
font-size: 2.3rem;
}
.list-item-label:first-letter {
text-transform: uppercase;
}
.value {
font-size: 1.8rem;
}
.subitem {
font-size: 1.9rem;
display: grid;
grid-template-columns: 4rem 7fr;
width: 100%;
padding: 1.5rem 0;
margin: 0;
cursor: default;
line-height: 2.5rem;
}
.subitem::before {
content: '✖';
color: rgba(0,0,0,0);
padding: 0.75rem 0.75rem;
max-height: 3rem;
max-width: 3rem;
margin-right: 1rem;
line-height: 1.5rem;
font-size: 1.5rem;
box-sizing: border-box;
background: #CCC;
grid-column: 1;
}
.done {
color: rgba(0,0,0,0.5);
}
.done::before {
color: rgba(0,0,0,0.5);
content: '✖';
text-align: center;
}
#custom-items {
display: none;
-webkit-transition: all .3s ease;
-moz-transition: all .3s ease;
-ie-transition: all .3s ease;
-o-transition: all .3s ease;
transition: all .3s ease;
}
#custom-item-field-wrap {
position: fixed;
display: grid;
grid-template-columns: 4fr 1fr;
bottom: 0;
left: 0;
padding: 0.75rem 1.5rem 1.5rem 1.5rem;
width: 100%;
background: white;
background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(255,255,255,0.5) 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(255,255,255,0.5) 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(255,255,255,0.5) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#80ffffff',GradientType=0 );
z-index: 200;
}
#custom-item-field-wrap p {
font-size: 1.5rem;
color: #444C62;
grid-column: span 2;
margin-bottom: 0.5rem;
}
#custom-item-field-wrap input {
border: 0;
padding: 1.5rem;
font-size: 2.5rem;
line-height: 2.5rem;
}
#custom-item-field-wrap input[type="text"] {
background: #DDD;
border-radius: 1rem 0 0 1rem;
font-weight: 500;
}
#custom-item-field-wrap input[type="submit"] {
border-radius: 0 1rem 1rem 0;
background: #CCC;
font-weight: 600;
font-size: 2rem;
}
#print-list {
display: none;
position: fixed;
overflow: auto;
top: 2rem;
bottom: 2rem;
left: 2rem;
right: 2rem;
background: white;
border-radius: 1rem;
z-index: 500;
font-size: 3rem;
padding: 2rem;
}
#print-list textarea {
height: calc(100% - 12rem);
width: 100%;
font-size: 3rem;
padding: 2rem;
border: 0;
border-radius: 1rem;
box-shadow: inset 0 1rem 1.5rem rgba(0,0,0,0.1);
}
#print-list p {
font-size: 2rem;
padding: 2rem;
margin: 2rem 0rem;
line-height: 2rem;
background: rgba(0,0,80,0.4);
color: white;
border-radius: 1rem;
text-align: center;
font-weight: 600;
cursor: default;
}
#copy-confirm {
display: none;
opacity: 0;
pointer-events: none;
transition: all 0.3s;
z-index: 2000;
color: #64767b;
padding: 6rem;
font-size: 2rem;
font-weight: 600;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
text-align: center;
background: #24363b;
background: -moz-linear-gradient(bottom, rgba(63,90,86,0.2) 30%, rgba(53,77,77,0.5) 100%);
background: -webkit-gradient(left bottom, left bottom, color-stop(30%, rgba(63,90,86,0.2)), color-stop(100%, rgba(53,77,77,0.5)));
background: -webkit-linear-gradient(bottom, rgba(63,90,86,0.2) 30%, rgba(53,77,77,0.5) 100%);
}
@media only screen and (min-width: 600px) {
/* hover styles for desktop only */
.button:hover {
background: rgba(60,90,255,1);
}
.button-light:hover {
background: transparent;
box-shadow: inset 0 0.5rem 2.5rem rgba(255,255,255,0.5), 0 0.5rem 1.5rem rgba(0,0,80,0.3);
color: #000022;
}
.button-startover:hover {
box-shadow: inset 0 0.5rem 2.5rem rgba(255,255,255,0.8), 0 0.5rem 1.5rem rgba(0,0,80,0.4);
}
#custom-item-field-wrap input[type="text"]:hover {
background: #F0F0F0;
box-shadow: inset 0 0.5rem 1rem rgba(0,0,0,0.1);
}
#custom-item-field-wrap input[type="submit"]:hover {
background: #78A7AA;
}
.subitem:hover {
background: #eee;
}
/* display styles */
html, body {
font-size: 12px;
background-position: 50% 25%;
}
#print-list, #page-list {
max-width: 550px;
margin-left: auto;
margin-right: auto;
}
#custom-item-field-wrap {
max-width: 750px;
left: 50%;
transform: translateX(-50%);
border-radius: 1rem 1rem 0 0;
}
}
$(document).ready(function() {
// EVENT HANDLERS:
$("#button-start").click(function() {
$('#page-start').hide(200);
$('#page1').show(200);
});
// there's got to be a better way to do this
// >:(
// Anyway when people/nights plus/minus are clicked they call inputStep(this), which then sorts depending on whether the string is found, and steps up or steps down the input accordingly.
function inputStep(el) {
var peopleNights = "";
var plusMinus = "";
if ( el.id.indexOf("people") != -1 ) {
peopleNights = 'people';
} else if ( el.id.indexOf("nights") != -1 ) {
peopleNights = 'nights';
}
if ( el.id.indexOf("plus") != -1 ) {
plusMinus = 'plus';
} else if ( el.id.indexOf("minus") != -1 ) {
plusMinus = 'minus';
}
if ( plusMinus === 'plus') { document.getElementById(peopleNights).stepUp(); }
else if ( plusMinus === 'minus' ) {
if ( peopleNights === 'people' ) {
if (document.getElementById("people").value > 1 ) {
document.getElementById('people').stepDown();
}
} else if ( peopleNights === 'nights' ) {
if (document.getElementById("nights").value > 0 ) {
document.getElementById('nights').stepDown();
}
}
}
}
$("#people-plus, #nights-plus, #people-minus, #nights-minus").click(function() {
inputStep(this);
});
$("#button-page1").click(function() {
$('#page1').hide(200);
$('#page2').show(200);
});
$("#button-page2").click(function() {
$('#page2').hide(200);
$('#page3').show(200);
});
$("#button-page3").click(function() {
$('#page3').hide(200);
$('#page4').show(200);
});
$("#button-generate").click(function() {
$('#page4').hide(200);
$('#page-list').show(200);
updateVars();
refineElements();
});
$("#button-startover").click(function() {
$('#custom-items .value').children().remove();
$('#custom-items').hide(200);
$('#page-list').hide(200);
$('#page-start').show(200);
});
// CREATING LIST:
var dataObject = {
"litres water": 3,
"calories food": 2000,
"food": [
"sausages",
"cheese",
"veggies",
"trail mix",
"granola bars"
],
"cooking stuff": [
"stove",
"stove fuel",
"tea",
"sponge",
"soap",
"cutlery",
"knife",
"tupperware",
"spices",
"lighter",
"can opener"
],
"personal items": [
"phone",
"book",
"journal",
"ukulele"
],
"camping stuff": [
"tent",
"sleeping bag",
"sleeping pad",
"bug spray",
"sunscreen",
"util knife",
"paracord",
"sewing kit"
],
"rain stuff": [
"tent fly",
"tarp",
"rainjacket",
"rainpants",
"dry bags"
],
"tire change stuff": [
"spare tube",
"patch kit",
"pump",
"tire levers",
"adjustable wrench"
],
"other bike tools": [
"adjustable wrench x2",
"zipties",
"multi grease",
"allen keys",
"pliers+screwdriver multitool"
],
"bike clothes": [
"sports bra",
"bike shorts",
"bike leggings",
"quick-dry fleece",
"quick-dry tank"
],
"night clothes": [
"wool sweater",
"leggings big",
"leggings small",
"fuzzy pjs",
"short-sleeve tee",
"long-sleeve tee"
],
"overnight items": [
"contact case + solution",
"earplugs",
"melatonin",
"journal",
"book",
"tp",
"headlamp",
"phone charger",
"spare batteries",
"bear spray",
"mp3 player",
"toothpaste and brush",
"hairbrush",
"shampoo",
"mini towel"
]
}
// create an element for each item in the dataObject. This will be refined later based on the value of the variables defined below.
createElements(dataObject);
function createElements(data) {
var list = document.getElementById("page-list");
// loop over each key-value pair to create elements to display them
$.each(data, function(index, value) {
var ElLabel, ElValue, wrap;
ElLabel = document.createElement("P");
ElValue = document.createElement("P");
wrap = document.createElement("P");
$(ElLabel).addClass("list-item-label");
$(ElValue).addClass("value");
$(wrap).addClass("wrap");
$(wrap).attr('id', index);
// if the value is an array (ie a group of other items), loop over that array and create subitems for each.
if ( $.isArray(value) ) {
ElLabel.innerHTML = index;
ElValue.innerHTML = "";
value.forEach(function(index) {
var ElItem = document.createElement("P");
$(ElItem).addClass("subitem");
ElItem.innerHTML = index;
$(ElItem).attr('id', index);
ElValue.appendChild(ElItem);
});
} else {
ElLabel.innerHTML = index;
ElValue.innerHTML = value;
}
wrap.appendChild(ElLabel);
wrap.appendChild(ElValue);
list.appendChild(wrap);
});
}
// set up variables that the list is based upon to be edited as the user gives input:
var biking = false;
var rain = false;
var people = 1;
var nights = 0;
$(":input").change(function() {
// don't let nights = less than 0
if ( $("#nights").val() < 0 ) {
$("#nights").val(0);
}
// don't let people = less than 1
if ( $("#people").val() < 1 ) {
$("#people").val(1);
}
// text toggles functionality:
if ( $(":input[value='biking']").prop("checked") ) {
$("#biking-yes").addClass("toggle-on");
$("#biking-no").removeClass("toggle-on");
} else {
$("#biking-no").addClass("toggle-on");
$("#biking-yes").removeClass("toggle-on");
}
if ( $(":input[value='rain']").prop("checked") ) {
$("#rain-yes").addClass("toggle-on");
$("#rain-no").removeClass("toggle-on");
} else {
$("#rain-no").addClass("toggle-on");
$("#rain-yes").removeClass("toggle-on");
}
});
// set the variables we defined above to the input given by the user:
function updateVars() {
biking = $(":input[value='biking']").prop("checked");
rain = $(":input[value='rain']").prop("checked");
people = parseInt( $("#people").val() );
nights = parseInt( $("#nights").val() );
}
// Use the variables to cut down the list of all elements that was generated in createElements
// note that some id's refer to wrapped groups of other items! for example, 'tire change stuff' is a named group of items. 'tire levers' would be an item inside that group. In this way I can toggle on/off groups and add/remove certain elements of that group depending on the variables.
function refineElements() {
if ( biking && nights === 0 ) {
$("[id='tire change stuff']").show();
$("[id='bike clothes']").show();
$("[id='other bike tools']").hide();
$("[id='ukulele']").hide();
} else if ( biking && nights > 0 ) {
$("[id='tire change stuff']").show();
$("[id='other bike tools']").show();
$("[id='bike clothes']").show();
$("[id='ukulele']").show();
} else {
$("[id='bike clothes']").hide();
$("[id='tire change stuff']").hide();
$("[id='other bike tools']").hide();
$("[id='ukulele']").show();
}
if ( rain && nights === 0 ) {
$("[id='rain stuff']").show();
$("[id='tent fly']").hide();
$("[id='tarp']").hide();
$("[id='dry bags']").hide();
} else if ( rain && nights > 0 ) {
$("[id='rain stuff']").show();
$("[id='tent fly']").show();
$("[id='tarp']").show();
$("[id='dry bags']").show();
} else {
$("[id='rain stuff']").hide();
}
if ( nights > 0 ) {
$("[id='night clothes']").show();
$("[id='camping stuff']").show();
$("[id='cooking stuff']").show();
$("[id='overnight items']").show();
$("[id='sausages']").show();
$("[id='cheese']").show();
$("[id='veggies']").show();
} else {
$("[id='night clothes']").hide();
$("[id='camping stuff']").hide();
$("[id='cooking stuff']").hide();
$("[id='overnight items']").hide();
$("[id='sausages']").hide();
$("[id='cheese']").hide();
$("[id='veggies']").hide();
}
var nightCalc, nightsMultiplier;
if ( nights === 0 ) { nightCalc = 1; nightsMultiplier = .5; } else { nightCalc = nights; nightsMultiplier = 1; }
$("[id='litres water'] .value").text(nightCalc * people * 3 * nightsMultiplier);
$("[id='calories food'] .value").text(nightCalc * people * 2000 * nightsMultiplier);
// make the Food list say "Food x [people]":
$("#food .list-item-label").text("Food x" + people);
}
// get and show list of items in plaintext so it can be printed / copied etc when list-print-list is clicked.
// gets list of all visible subitems, pushes their value to an array, opens the modal with the textarea, prints the array to the textarea on new lines.
$("#list-print-list").click(function() {
var printableList = [];
var totalShown = $(".subitem:visible");
$.each( totalShown, function(index, value) {
printableList.push( $(value).text() );
});
$("#print-list").show(200);
$("#print-list textarea").text(printableList.join("\n"));
});
$("#print-list textarea").focus( function() {
this.select();
document.execCommand("copy");
$("#copy-confirm").show();
$("#copy-confirm").css("opacity", "1");
$("#copy-confirm").delay(1000).queue(function(){
$(this).css("opacity", "0").dequeue();
$(this).hide(300).dequeue;
});
});
// LIST FUNCTIONALITY:
// 'check off' a subitem when it's clicked.
// just adds or removes the class 'done'. Done uses ::before to show a checkmark over subitem::before's checkbox area.
// Then it calls checkDone to see if that group has been completed.
$('body').on('click', '.subitem', function() {
$(this).toggleClass("done");
checkDone( $(this) );
});
// When a subitem is clicked, check to see if all the subitems in that group are checked off, and fade their group if so.
// get number of subitems with class .done - ie number of checked off subitems - in current group
// get number of total VISIBLE items in that group
// if they're the same, then all items in that group are checked off, and the element is faded. Otherwise, it's unfaded.
function checkDone(thisElement) {
var numDone = thisElement.parent().find( $(".done") ).length;
var numShown = thisElement.parent().find( ":visible" ).length;
if ( numDone === numShown ) {
thisElement.closest( $(".wrap") ).fadeTo("fast", 0.5);
} else {
thisElement.closest( $(".wrap") ).fadeTo("fast", 1);
}
}
// CUSTOM ITEMS:
$("#custom-item-add").click( function() {
customItemAdd();
$("body, html").scrollTop( $("#custom-items").offset().top );
});
// Add custom item to the list of generated items.
function customItemAdd() {
// prevent default or the submit button will take us somewhere! But it's needed for submitting form with enter.
event.preventDefault();
// Show the custom-items wrap div if it isn't shown already.
if ( !$("#custom-items").show() && $("#custom-item-value").val() ) {
$("#custom-items").show(200);
}
// make the div glow a bit so you can tell something has happened
$("#custom-items").css("boxShadow", "0 0 5rem rgba(255,255,0,0.7)");
$("#custom-items").delay(300).queue(function(){
$(this).css("boxShadow", "0 0 0rem rgba(255,255,0,0.7)").dequeue();
});
// create a new subitem in that div, give it the value from the custom-item-value box. Clear that box. Check to see if custom-items is all checked off (because it isn't, because we JUST added something, so this will reset it)
if ( $("#custom-item-value").val() ) {
customEl = document.createElement("p");
$(customEl).addClass("subitem");
$(customEl).text( $("#custom-item-value").val() );
document.querySelector("#custom-items .value").appendChild( customEl );
$("#custom-item-value").val('');
checkDone( $(customEl) );
}
}
});
This Pen doesn't use any external CSS resources.