├── EX1-CrazyMike's.eve ├── EX2-Sandlot.eve ├── EX3-JurassicPark.eve ├── EX4-VoterRegistration.eve ├── EX5-BattleSchool.eve ├── LICENSE ├── README.md ├── assets ├── fr-bold.ttf ├── fr-fal1.ttf ├── fr-fal2.ttf └── fr-title.ttf └── package.json /EX1-CrazyMike's.eve: -------------------------------------------------------------------------------- 1 | # Crazy Mike's 2 | 3 | ```css 4 | {there is currently a bug that causes the first CSS block in an Eve program to be disregarded, so for a good time, leave this here} 5 | ``` 6 | 7 | ## What is this? 8 | 9 | This small app is pretty straightforward, consisting of a simple webpage with four subpages. The purpose is to demonstrate some basic webpage structure, show how a navigation bar could be implemented, how it changes the view between the different subpages, and how to inject page contents into the page view as you navigate from one subpage to another. 10 | 11 | ## Page Layout 12 | 13 | ### Containers 14 | 15 | I want this app to have three containers: a hero image, a nav bar above the page container, and page contents which are going to change depending on the active section. I draw the basic page structure here and worry about details like drawing the individual tabs for the subpages later. The hero image and the nav bar also have their classes set here because their style never changes. The individual pages may require different styles, so their classes are bound later. 16 | 17 | ``` 18 | commit @browser 19 | [#div class:"app-wrapper" children: 20 | [#div class:"hero-image"] 21 | [#div class:"nav-bar" #nav-bar] 22 | [#div #page-contents]] 23 | ``` 24 | 25 | ### Subpages 26 | 27 | Crazy Mike sells a modest selection of repossessed electronics. I can set all the pages the site is going to have right here, and because I separated this from the page containers above, if I want to add another page and have it appear as a tab on the navigation bar, I can simply add it to this list. 28 | 29 | Each page has a name, which is displayed as a label on the navigation button; and an order, which indicates its position in the nav bar. 30 | 31 | ``` 32 | commit 33 | [#page name: "Home" order: 1] 34 | [#page name: "Televisions" order: 3] 35 | [#page name: "Computers" order: 2] 36 | [#page name: "Stereos" order: 4] 37 | ``` 38 | 39 | ### Initial Landing Site 40 | 41 | The #`app` record is where I've decided to keep track of which page is being viewed. I've also set `page` to `homepage` to begin with so that new customers will land there when they visit the site. 42 | 43 | ``` 44 | commit 45 | [#app page:"Home"] 46 | ``` 47 | 48 | ## The Navigation Bar 49 | 50 | ### Drawing the Nav Bar 51 | 52 | While the hero image was easy, the nav bar gets its own section because it needs a little more love than a background. It needs to make a button for each page of the website, which were committed in the Subpages section, so I take all the #`page` records and add a child #`div` to the #`nav-bar` for each of them. 53 | 54 | ``` 55 | search @session @browser 56 | page = [#page] 57 | nav-bar = [#nav-bar] 58 | 59 | bind @browser 60 | nav-bar.children += [#div sort: page.order, class:"nav-btn", page text: page.name] 61 | ``` 62 | 63 | ### Navigation 64 | 65 | We start on the home page, but when you click a button on the nav bar, we want to navigate to that page. This listens for a click on any nav button, whose record has a page attached to it, then sets the `page` attribute of the #`app` record to match the `page` attribute of the button that was clicked. 66 | 67 | ``` 68 | search @browser @session @event 69 | click = [#click element:[#div page class:"nav-btn"]] 70 | view = [#app] 71 | 72 | commit 73 | view.page := page.name 74 | ``` 75 | 76 | ### Highlighting the Active Page 77 | 78 | Purely as a style issue, I want to change the background color of the nav bar button of whichever page we're on. You could also use this block to bind a new class to `nav-btn` and use CSS to set the new background color, but this is a little more terse without obfuscating the goal. 79 | 80 | ``` 81 | search @session @browser 82 | [#app page] 83 | nav-btn = [#div page class:"nav-btn"] 84 | 85 | bind @browser 86 | nav-btn.style += [background:"#606060"] 87 | ``` 88 | 89 | ## Subpages 90 | 91 | ### Home Page 92 | 93 | When the app specifies that we should be looking at the home page, the contents of this block are injected into the #`page-contents` record that was bound to the browser at the start of the Page Layout section. 94 | 95 | ``` 96 | search @session @browser 97 | [#app page: "Home"] 98 | view = [#page-contents] 99 | 100 | bind @browser 101 | view <- [class: "main-page" children: 102 | [#h1 text: "Welcome to Crazy Mike's!"] 103 | [#p text: "Located on the scenic Pulaski Highway in East Baltimore, Crazy Mike's has the region's best selection of used electronics, and our prices are INSANE!"] 104 | [#p text: "Hours: Tue-Sat 2pm-4am"] 105 | [#p text: "Contact: (410) 768-7000, Ask for Mike"]] 106 | ``` 107 | 108 | ### Computers 109 | 110 | Much like the home page, when the app specifies that we want to navigate to the Computers tab, we want to inject this page into #`page-contents`. 111 | 112 | ``` 113 | search @session @browser 114 | [#app page: "Computers"] 115 | view = [#page-contents] 116 | 117 | bind @browser 118 | view <- [class: "main-page" children: 119 | [#p text: "Need to compute things? We can help."] 120 | [#div class:("computer", "pic")] 121 | [#p text: "One of our many fine products, this War Operations Plan Response supercomputer was repossessed from the US Dept. of Defense in 1984. Comes with classic games such as chess, checkers, backgammon, poker, tic-tac-toe, and Global Thermonuclear War, though it has been known not to play. Open box, comes as-is. Strict no return policy."]] 122 | ``` 123 | 124 | ### Televisions 125 | 126 | Once more, when we navigate to the Televisions tab, it gets injected into #`page-contents`. 127 | 128 | ``` 129 | search @session @browser 130 | [#app page: "Televisions"] 131 | view = [#page-contents] 132 | 133 | bind @browser 134 | view <- [class:"main-page" children: 135 | [#p text: "This is where the TVs live - get you one!"] 136 | [#div class: "tv pic"] 137 | [#p text: "Forget the internet, this baby is the real series of tubes. Perfect for your LaserDisc collection."] ] 138 | ``` 139 | 140 | ### Stereos 141 | 142 | When we navigate to the Stereos tab, it gets injected into #`page-contents`. 143 | 144 | ``` 145 | search @session @browser 146 | [#app page: "Stereos"] 147 | view = [#page-contents] 148 | 149 | bind @browser 150 | view <- [class: "main-page" children: 151 | [#p text: "The hottest audio equipment in town!"] 152 | [#div class: "radio pic"] 153 | [#p text: "New stock arriving daily, priced to move."] 154 | ] 155 | ``` 156 | 157 | ## Styles 158 | 159 | A little CSS to clean things up and make the page more readable. 160 | 161 | ```css 162 | .app-wrapper { 163 | display: flex; 164 | flex-direction: column; 165 | position: absolute; 166 | align-self: center; 167 | width: 432px; 168 | height: 768px; 169 | overflow-y: auto; 170 | background: #fff; 171 | } 172 | 173 | .hero-image { 174 | height: 300px; 175 | background-image: url(http://i.imgur.com/L8suaDZ.jpg); 176 | background-size: cover; 177 | } 178 | 179 | .nav-bar { 180 | display: flex; 181 | flex-direction: row; 182 | width: 100%; 183 | height: 50px; 184 | align-items: center; 185 | } 186 | 187 | .nav-btn { 188 | flex: 1; 189 | text-align: center; 190 | background: #404040; 191 | line-height: 50px; 192 | color: #fff; 193 | user-select: none; 194 | cursor: pointer; 195 | } 196 | 197 | .main-page { 198 | display: flex; 199 | flex-direction: column; 200 | align-items: center; 201 | padding-top: 20px; 202 | background: ; 203 | } 204 | 205 | .main-page h1 { 206 | margin: 10px 0px; 207 | font-size: 26px; 208 | } 209 | 210 | .main-page p { 211 | margin: 5px 25px; 212 | font-size: 16px; 213 | text-align: center; 214 | } 215 | 216 | .pic { 217 | margin: 20px 0px; 218 | } 219 | 220 | .computer { 221 | height: 150px; 222 | width: 320px; 223 | background: url(http://i.imgur.com/2EbQYGA.jpg) center no-repeat; 224 | background-size: cover; 225 | } 226 | 227 | .tv { 228 | height: 250px; 229 | width: 320px; 230 | background: url(http://i.imgur.com/0kD08WU.jpg) center no-repeat; 231 | background-size: cover; 232 | } 233 | 234 | .radio { 235 | height: 250px; 236 | width: 320px; 237 | background: url(http://i.imgur.com/rs1NW31.jpg) center no-repeat; 238 | background-size: cover; 239 | } 240 | ``` 241 | -------------------------------------------------------------------------------- /EX2-Sandlot.eve: -------------------------------------------------------------------------------- 1 | # The Sandlot 2 | 3 | ```css 4 | {there is currently a bug that causes the first CSS block in an Eve program to be disregarded, so for a good time, leave this here} 5 | ``` 6 | 7 | ## What is this? 8 | 9 | This app demonstrates how to create a reusable component and how to inject it into the browser. This app displays a batting order for a baseball team using a `#player-card` component. This component displays information relating to each player on a baseball team -- picture, name, position, and handedness. A `#player-card` can also be configured with additional functionality using optional tags. The app also shows some of the ways `sort` can be used, and how to choose specific records out of a list of records. 10 | 11 | ## Page Layout 12 | 13 | ### Containers 14 | 15 | I want to break the page down into two sections: one for the active lineup and the bench players, and one for players on injured reserve. 16 | 17 | ``` 18 | commit @browser 19 | [#div class:"container" children: 20 | [#div #active class:"lineup" text:"Batting Order"] 21 | [#div #inactive class:"lineup" text:"Bench"]] 22 | [#div #IR class:"lineup IR" text:"Injured Reserve"] 23 | ``` 24 | 25 | ### Active Lineup 26 | 27 | The first column drawn should be the active batting order. While I could have specified all the text and contents of each player in this block, note that I used two tags, `#player-card` and `#moveable`, whose contents and behavior get defined later. Since every player listed is going to have the same information, `#player-card` lets me drop the tag in and define all the HTML and content later, which is done in the Player Cards section after this one. 28 | 29 | ``` 30 | search @session @browser 31 | player = [#player not(#bench) not(#injured) lineup] 32 | active = [#active] 33 | 34 | bind @browser 35 | active.children += [#div #lineup children: [#player-card #moveable player sort: lineup]] 36 | ``` 37 | 38 | ### Bench Players 39 | 40 | These are the bench players. Note that `#player-card` is used again, saving me the work of having to define all the same html and content that's used in the active lineup. The big difference here is the ordering of the players. Whereas the players in the lineup have a deliberate and specific order (which makes sense for things like a batting order), I don't need a specific order for the bench players. I do, however, want to know how many there are and make sure they render in the same order each time, so I sort by the `player` variable to get an index, `n`. Each player gets assigned an index, which is how I sort the bench players, and also set `player.lineup` to that index so they get numbered 1, 2, 3, and so on. 41 | 42 | ``` 43 | search @session @browser 44 | player = [#player #bench lineup] 45 | inactive = [#inactive] 46 | n = sort[value:player] 47 | 48 | bind @session @browser 49 | player.lineup := n 50 | inactive.children += [#div #lineup children: [#player-card #benched sort:n player]] 51 | ``` 52 | 53 | ### Injured Reserve 54 | 55 | These are the injured reserve players. This section is functionally identical to the bench player section, except it gets merged into the `#IR` section on the page. There's only one player on the IR and no functionality exists to move him elsewhere, because he is a jerk. 56 | 57 | ``` 58 | search @session @browser 59 | player = [#player #injured not(#bench) lineup] 60 | IR = [#IR] 61 | n = sort[value:player] 62 | 63 | bind @session @browser 64 | player.lineup := n 65 | IR.children += [#div #lineup children: [#player-card sort:n player]] 66 | ``` 67 | 68 | ## Player Cards 69 | 70 | This is where the magic happens. Every time this finds `#player-card` in the browser, it will make a `#div` for each player and spit out a player card. It checks to see if the player has a nickname as well, and just leaves a space between the first and last names if there isn't. It's worth mentioning that because I want this to be as general a template as possible, a handful of architectural decisions were made to facilitate that. In the sections that draw each of the three lists, I use the search parameters there to select the specific players I want and inject those player cards. In the Roster Data, all the players have a `lineup` attribute even though the bench and IR players have an empty string as the value. If you're good at predicting which reusable parts you'll need, you can implement these sorts of things from the outset, or you can figure out where they need to be as you go along and refactor them into your code as you work. 71 | 72 | ``` 73 | search @browser @session 74 | container = [#player-card player] 75 | player = [#player firstname lastname position bats lineup photo] 76 | middle = if player.nickname then " \"{{player.nickname}}\" " 77 | else " " 78 | 79 | bind @browser 80 | container <- [#div player class: "lineup-position" children: 81 | [#div class: "bat-number" text: lineup] 82 | [#div class: "player-info" children: 83 | [#img class: "player-photo" src: photo] 84 | [#div class: "player-text" children: 85 | [#div class: "name-line" children: 86 | [#div class: "position" text: position] 87 | [#div class: "first-name" text: firstname] 88 | [#div class: "middle-name" text: middle] 89 | [#div class: "last-name" text: lastname]] 90 | [#div class:"bat-hand" text:"Bats: {{bats}}"]]]] 91 | ``` 92 | 93 | ## Buttons 94 | 95 | ### Move Up 96 | 97 | The active roster also features a cool way to use tags. Since I want to be able to reorder my active lineup, each `#player-card` there is also tagged `#moveable`. This checks to see if a `#player-card` is `#moveable` and adds a child `#div` to it with a `#move-up-btn`, unless that player is already at the top of roster and can't move up any further. 98 | 99 | ``` 100 | search @browser @session 101 | container = [#div #moveable player] 102 | not(player.lineup = 1) 103 | 104 | bind @browser 105 | container.children += [#div #move-up-btn player class: "ion-chevron-up"] 106 | ``` 107 | 108 | It's like hashtag inception. Just as the `#moveable` tag let Eve know to add a `#move-up-btn`, this block adds another layer of functionality by giving that button some behavior. In this case, when a `#move-up-btn` is clicked, Eve captures who the specific `clicked-player` is, finds whoever is one space above them in the lineup, and switches their places. 109 | 110 | ``` 111 | search @browser @event @session 112 | [#click element: [#move-up-btn player: clicked-player]] 113 | above-clicked = [#player lineup: clicked-player.lineup - 1] 114 | 115 | commit 116 | clicked-player.lineup := clicked-player.lineup - 1 117 | above-clicked.lineup := above-clicked.lineup + 1 118 | ``` 119 | 120 | ### Move Down 121 | 122 | This block is the mirror of the move up buttons. If a `#player-card` is `#moveable`, and not at the bottom of the lineup, which is what the `count` is checking, then a child `#div` gets added with a `#move-down-btn`. 123 | 124 | ``` 125 | search @browser @session 126 | container = [#div #moveable player] 127 | not(player.lineup: count[given: [#player not(#bench) not(#injured)]]) 128 | 129 | bind @browser 130 | container.children += [#div #move-down-btn player class: "ion-chevron-down"] 131 | ``` 132 | 133 | Once again the reciprocal of the previous section, when a `#move-down-btn` is clicked, Eve captures the specific `clicked-player`, finds the player one space below them in the lineup, and switches their places. 134 | 135 | ``` 136 | search @browser @event @session 137 | [#click element:[#move-down-btn player: clicked-player]] 138 | below-clicked = [#player lineup: clicked-player.lineup + 1] 139 | 140 | commit 141 | clicked-player.lineup := clicked-player.lineup + 1 142 | below-clicked.lineup := below-clicked.lineup - 1 143 | ``` 144 | 145 | ### Remove from Lineup 146 | 147 | If a player is `#moveable`, it means they're in the active lineup and should be able to be removed from the active lineup and sent to the bench. This simply looks for any `#moveable` players and adds a `#deactivate` button to them. 148 | 149 | ``` 150 | search @browser @session 151 | container = [#div #moveable player] 152 | 153 | bind @browser 154 | container.children += [#div #deactivate player class: "ion-log-out"] 155 | ``` 156 | 157 | This gives `#deactivate` its behavior. The `clicked-player` is sent to the bench and is stripped of their lineup order. All the players who were below them on the lineup get moved up a spot so that there aren't gaps in the numbering. 158 | 159 | ``` 160 | search @browser @event @session 161 | [#click element: [#deactivate player: clicked-player]] 162 | players-below = [#player lineup > clicked-player.lineup] 163 | 164 | commit 165 | clicked-player += #bench 166 | clicked-player.lineup := "" 167 | players-below.lineup := players-below.lineup - 1 168 | ``` 169 | 170 | This block takes care of an edge case for `#deactivate`. Because the previous block finds all the `players-below` the `clicked-player`, if the player is at the bottom of the lineup then there are no `players-below` and the search block fails, preventing the button from doing anything. This makes sure you can move the ninth batter off the lineup onto the bench. 171 | 172 | ``` 173 | search @browser @event @session 174 | [#click element: [#deactivate player: clicked-player]] 175 | 176 | commit 177 | clicked-player += #bench 178 | clicked-player.lineup := "" 179 | ``` 180 | 181 | ### Add to Lineup 182 | 183 | This adds an `#activate` button to the bench players so we can add them to the active lineup, but only if there are fewer than nine batters already in the lineup. 184 | 185 | ``` 186 | search @browser @session 187 | container = [#div #benched player] 188 | active-players = if c = count[given: [#player not(#bench) not(#injured)]] then c else 0 189 | active-players != 9 190 | 191 | bind @browser 192 | container.children += [#div #activate player class:"ion-log-in"] 193 | ``` 194 | 195 | This gives `#activate` its behavior. When an `#activate` button is clicked, Eve finds out how many active players are in the lineup then adds the `clicked-player` and assigns them the next number in the lineup. 196 | 197 | ``` 198 | search @browser @event @session 199 | [#click element: [#activate player:clicked-player]] 200 | active-players = if c = count[given:[#player not(#bench) not(#injured)]] then c else 0 201 | active-players != 9 202 | 203 | commit 204 | clicked-player -= #bench 205 | clicked-player.lineup := active-players + 1 206 | ``` 207 | 208 | ## Roster Data 209 | 210 | Our sample roster data. 211 | 212 | ``` 213 | commit 214 | [#player firstname:"Kenny" lastname:"DeNunez" position:"P" bats:"R" lineup:3 photo:"http://i.imgur.com/kaRnA7R.png"] 215 | [#player firstname:"Hamilton" lastname:"Porter" nickname:"Ham" position:"C" bats:"R" lineup:1 photo:"http://i.imgur.com/7C678n2.jpg"] 216 | [#player firstname:"Timmy" lastname:"Timmons" position:"IF" bats:"R" lineup:8 photo:"http://i.imgur.com/JLHupGR.png"] 217 | [#player firstname:"Bertram" lastname:"Grover Weeks" position:"IF" bats:"R" lineup:2 photo:"http://i.imgur.com/mfIMIy6.png"] 218 | [#player firstname:"Alan" lastname:"McClennan" nickname:"Yeah-Yeah" position:"IF" bats:"R" lineup:6 photo:"http://i.imgur.com/ZyLzCDn.jpg"] 219 | [#player firstname:"Benny" lastname:"Rodriguez" nickname:"The Jet" position:"IF" bats:"S" lineup:9 photo:"http://i.imgur.com/UOywuNz.jpg"] 220 | [#player firstname:"Scott" lastname:"Smalls" position:"OF" bats:"R" lineup:5 photo:"http://i.imgur.com/eBZ2m17.jpg"] 221 | [#player firstname:"Michael" lastname:"Palledorous" nickname:"Squints" position:"OF" bats:"R" lineup:4 photo:"http://i.imgur.com/q8KKRz6.jpg"] 222 | [#player firstname:"Tommy" lastname:"Timmons" nickname:"Repeat" position:"OF" bats:"R" lineup:7 photo:"http://i.imgur.com/QPzSCGy.png"] 223 | [#player #bench firstname:"Thelonius" lastname:"Mertle" position:"IF" bats:"L" lineup:"" photo:"http://i.imgur.com/XDA0ftH.jpg"] 224 | [#player #bench firstname:"George Herman" lastname:"Ruth" nickname:"Babe" position:"P" bats:"L" lineup:"" photo:"http://i.imgur.com/kep7Unm.jpg"] 225 | [#player #bench firstname:"Hercules" lastname:"Mertle" nickname:"The Beast" position:"PR" bats:"S" lineup:"" photo:"http://i.imgur.com/WOwMn5c.jpg"] 226 | [#player #injured firstname:"" lastname:"Phillips" position:"IF" bats:"R" lineup:"" photo:"http://i.imgur.com/Qvxya5C.jpg"] 227 | ``` 228 | 229 | ## Styles 230 | 231 | The app needs a good bit of CSS to organize the page sections and various buttons as well as stylize the player cards. 232 | 233 | ```css 234 | .container { 235 | display: flex; 236 | flex-direction: row; 237 | user-select: none; 238 | font-size: 20px; 239 | text-transform: uppercase; 240 | text-align: center; 241 | overflow: scroll; 242 | height: 1000px; 243 | border-bottom: 1px solid #555; 244 | flex: 1 1 auto; 245 | } 246 | 247 | .IR { 248 | width: 515px; 249 | margin-top: 30px; 250 | flex: 1 0 auto; 251 | } 252 | 253 | .lineup { 254 | display: flex; 255 | flex-direction: column; 256 | font-size: 20px; 257 | text-transform: uppercase; 258 | text-align: center; 259 | margin-right: 50px; 260 | position: relative; 261 | flex: 1 0 515; 262 | width: 515px; 263 | min-width: 515px; 264 | overflow: scroll; 265 | } 266 | 267 | .lineup-position { 268 | list-style: none; 269 | display: flex; 270 | flex-direction: row; 271 | align-items: center; 272 | margin-top: 15px; 273 | position: relative; 274 | } 275 | 276 | .bat-number { 277 | order: 1; 278 | font-size: 40px; 279 | font-weight: bold; 280 | color: #b4b4b4; 281 | margin-right: 15px; 282 | width: 50px; 283 | } 284 | 285 | .player-info { 286 | display: flex; 287 | flex-direction: row; 288 | margin: 0px 0px; 289 | padding: 0px 0px 0px 0px; 290 | height: 85px; 291 | width: 450px; 292 | background: #ffffff; 293 | border: 1px solid #555; 294 | border-radius: 8px; 295 | order: 2; 296 | overflow: hidden; 297 | } 298 | 299 | .name-line { 300 | display: flex; 301 | flex-direction: row; 302 | margin: 10px 10px; 303 | } 304 | 305 | .position { 306 | font-size: 14px; 307 | font-weight: bold; 308 | margin-right: 8px; 309 | padding-top: 2px; 310 | height: 16px; 311 | } 312 | 313 | .first-name { 314 | font-size: 16px; 315 | text-transform: uppercase; 316 | height: 16px; 317 | } 318 | 319 | .middle-name { 320 | font-size: 16px; 321 | text-transform: uppercase; 322 | height: 16px; 323 | font-weight: 600; 324 | white-space: pre; 325 | } 326 | 327 | .last-name { 328 | font-size: 16px; 329 | text-transform: uppercase; 330 | height: 16px; 331 | 332 | } 333 | 334 | .player-photo { 335 | height: 85px; 336 | width: 85px; 337 | border-right: 1px solid #555; 338 | } 339 | 340 | .bat-hand { 341 | font-size: 14px; 342 | height: 14px; 343 | text-align: left; 344 | margin: 10px; 345 | } 346 | 347 | .ion-chevron-up { 348 | position: absolute; 349 | top: 2px; 350 | right: 15px; 351 | font-size: 24px; 352 | cursor: pointer; 353 | } 354 | 355 | .ion-chevron-down { 356 | position: absolute; 357 | bottom: 2px; 358 | right: 15px; 359 | font-size: 24px; 360 | cursor: pointer; 361 | } 362 | 363 | .ion-log-out { 364 | position: absolute; 365 | right: 15px; 366 | color: #e65b3c; 367 | font-size: 24px; 368 | cursor: pointer; 369 | } 370 | 371 | .ion-log-in { 372 | position: absolute; 373 | right: 15px; 374 | transform: scaleX(-1); 375 | color: #009ee0; 376 | font-size: 24px; 377 | cursor: pointer; 378 | } 379 | ``` 380 | 381 | ```css 382 | @media (max-width: 1848px) { 383 | 384 | .container { 385 | flex-direction: column; 386 | border-bottom: none; 387 | height: auto; 388 | flex: 0 0 auto; 389 | } 390 | 391 | .IR { 392 | width: 515px; 393 | margin-top: 0px; 394 | } 395 | 396 | .lineup { 397 | display: flex; 398 | font-size: 18px; 399 | margin-right: 0px; 400 | flex: 0 0 auto; 401 | width: 415px; 402 | min-width: 415px; 403 | min-height: 100px; 404 | margin-bottom: 20px; 405 | } 406 | 407 | .lineup-position { 408 | margin-top: 12px; 409 | } 410 | 411 | .bat-number { 412 | font-size: 30px; 413 | margin-right: 15px; 414 | width: 50px; 415 | } 416 | 417 | .player-info { 418 | height: 45px; 419 | width: 400px; 420 | } 421 | 422 | .name-line { 423 | margin: 5px 8px; 424 | } 425 | 426 | .position { 427 | font-size: 10px; 428 | margin-right: 8px; 429 | } 430 | 431 | .first-name { 432 | font-size: 12px; 433 | } 434 | 435 | .middle-name { 436 | font-size: 12px; 437 | } 438 | 439 | .last-name { 440 | font-size: 12px; 441 | } 442 | 443 | .player-photo { 444 | height: 45px; 445 | width: 45px; 446 | } 447 | 448 | .bat-hand { 449 | font-size: 10px; 450 | margin: 5px 8px; 451 | } 452 | 453 | .ion-chevron-up { 454 | top: 0px; 455 | right: 15px; 456 | font-size: 16px; 457 | } 458 | 459 | .ion-chevron-down { 460 | bottom: 0px; 461 | right: 15px; 462 | font-size: 16px; 463 | } 464 | 465 | .ion-log-out { 466 | right: 15px; 467 | font-size: 16px; 468 | } 469 | 470 | .ion-log-in { 471 | right: 15px; 472 | font-size: 16px; 473 | } 474 | 475 | } 476 | ``` 477 | 478 | ```css 479 | @media (max-width: 1200px) { 480 | 481 | .container { 482 | flex-direction: column; 483 | border-bottom: none; 484 | height: auto; 485 | flex: 0 0 auto; 486 | } 487 | 488 | .IR { 489 | width: 515px; 490 | margin-top: 0px; 491 | } 492 | 493 | .lineup { 494 | display: flex; 495 | font-size: 18px; 496 | margin-right: 0px; 497 | flex: 0 0 auto; 498 | width: 100%; 499 | min-width: 150px; 500 | min-height: 100px; 501 | margin-bottom: 20px; 502 | } 503 | 504 | .lineup-position { 505 | margin-top: 12px; 506 | } 507 | 508 | .bat-number { 509 | font-size: 30px; 510 | margin-right: 0px; 511 | width: 0px; 512 | } 513 | 514 | .player-info { 515 | height: 55px; 516 | width: 100%; 517 | } 518 | 519 | .name-line { 520 | margin: 2px 8px; 521 | margin-top: 5px; 522 | flex-wrap: wrap; 523 | } 524 | 525 | .position { 526 | font-size: 8px; 527 | margin-right: 5px; 528 | } 529 | 530 | .first-name { 531 | font-size: 10px; 532 | } 533 | 534 | .middle-name { 535 | font-size: 10px; 536 | } 537 | 538 | .last-name { 539 | font-size: 10px; 540 | } 541 | 542 | .player-photo { 543 | height: 55px; 544 | width: 55px; 545 | } 546 | 547 | .bat-hand { 548 | font-size: 10px; 549 | margin: 0px 8px; 550 | } 551 | 552 | .ion-chevron-up { 553 | top: 7px; 554 | right: 8px; 555 | font-size: 12px; 556 | } 557 | 558 | .ion-chevron-down { 559 | bottom: 6px; 560 | right: 8px; 561 | font-size: 12px; 562 | } 563 | 564 | .ion-log-out { 565 | right: 8px; 566 | font-size: 12px; 567 | } 568 | 569 | .ion-log-in { 570 | right: 8px; 571 | font-size: 12px; 572 | } 573 | 574 | } 575 | ``` 576 | -------------------------------------------------------------------------------- /EX3-JurassicPark.eve: -------------------------------------------------------------------------------- 1 | # Jurassic Park Terminal 2 | 3 | ## What is this? 4 | 5 | This app demonstrates the use of a custom form component in a basic log in application. The app contains three views: a login form, a registration form, and a user profile form that displays the information entered during registration. These forms are built using a custom form component, which is defined at the end of the program. The form component allows for the concise definition of forms by defining common behavior like form submission and resetting. 6 | 7 | ## Application Set Up 8 | 9 | The app contains the current page, as well as the current user. Initially, though, there is no user, so we just need to specify the current page. 10 | 11 | ``` 12 | bind @browser 13 | [#div #app class: "app-wrapper" page: "login" children: 14 | [#div sort: 0 text: "Jurassic Park System Security Interface"]] 15 | ``` 16 | 17 | ## Pages 18 | 19 | ### Log In Form 20 | 21 | The log in form contains two input boxes, one for the username and another for a password. We must explicitly sort the fields (using a `sort` attribute) to display them in a specific order. 22 | 23 | ``` 24 | search @browser 25 | app = [#app page: "login"] 26 | 27 | bind @browser 28 | app.children += 29 | [#div sort: 1 children: 30 | [#form name: "Login" sections: 31 | [#section fields: 32 | [#field sort: 2 type: "password" field: "password"] 33 | [#field sort: 1 type: "input" field: "username"]]] 34 | [#button #signup text: "Sign Up" ]] 35 | ``` 36 | 37 | A successful login is one where the username and password entered in the login form match some user/password combination stored in the system. For simplicity, passwords are stored as plain text, so we just need to search for a `#user` with a matching username and password. If one is found, we set it as the user attribute in the `#app` record. 38 | 39 | ```eve 40 | search @browser @session 41 | [#form name: "Login" submission: [username password]] 42 | user = [#user username password] 43 | app = [#app] 44 | 45 | commit @browser 46 | app.page := "profile" 47 | app.user := user 48 | ``` 49 | 50 | If the user enters a login that does not match a user, then display a message indicating that the login failed. 51 | 52 | ```eve 53 | search @browser @session 54 | form = [#form name: "Login" submission: [username password]] 55 | form-message = [#form-message form] 56 | 57 | not([#user username password]) 58 | 59 | commit @browser 60 | form-message.text := "** Login Failed **" 61 | ``` 62 | 63 | Clicking the sign up button changes the page to the sign up page 64 | 65 | ``` 66 | search @browser @event 67 | [#click element: [#signup]] 68 | app = [#app] 69 | 70 | commit @browser 71 | app.page := "signup" 72 | ``` 73 | 74 | ### Sign Up Form 75 | 76 | The user registration page requests the name, department, a username and password. 77 | 78 | ``` 79 | search @browser 80 | app = [#app page: "signup"] 81 | 82 | bind @browser 83 | app.children += 84 | [#div children: 85 | [#form name: "Sign Up" options: [reset: true] sections: 86 | [#section name: "User Info" fields: 87 | [#field sort: 1 type: "input" field: "full-name"] 88 | [#field sort: 2 type: "input" field: "department"]] 89 | [#section name: "Account Info" fields: 90 | [#field sort: 1 type: "input" field: "username"] 91 | [#field sort: 2 type: "password" field: "password"] 92 | [#field sort: 3 type: "password" field: "confirm-password"]]] 93 | [#button #login text: "Log In" ]] 94 | ``` 95 | 96 | We need to create a `#user` from the submission of the registration form. This will only work if every field has an entry, and the two password fields match. 97 | 98 | ``` 99 | search @browser 100 | [#form name: "Sign Up" submission: [username password confirm-password full-name department]] 101 | // The password and the confirmation must match 102 | password = confirm-password 103 | app = [#app] 104 | 105 | commit @browser 106 | app.page := "login" 107 | 108 | commit 109 | [#user username password name: full-name department] 110 | ``` 111 | 112 | Clicking the login button changes the page back to the login screen 113 | 114 | ``` 115 | search @browser @event 116 | [#click element: [#login]] 117 | app = [#app] 118 | 119 | commit @browser 120 | app.page := "login" 121 | ``` 122 | 123 | ### Profile Page 124 | 125 | The profile page displays information relating to the current user profile. It is accessed after a successful submission of the login form, which creates a user attribute in the #app. 126 | 127 | ``` 128 | search @browser @session 129 | app = [#app page: "profile" user] 130 | 131 | bind @browser 132 | app.children += 133 | [#div class: "profile" children: 134 | [#div text: "Welcome {{user.username}}!"] 135 | [#div text: "Name: {{user.name}}"] 136 | [#div text: "Department: {{user.department}}"] 137 | [#button #logout text: "Log Out"]] 138 | ``` 139 | 140 | Clicking logout returns to the login page, and removes the user from `#app`. 141 | 142 | ``` 143 | search @browser @event 144 | [#click element: [#logout]] 145 | app = [#app] 146 | 147 | commit @browser 148 | app.user := none 149 | app.page := "login" 150 | ``` 151 | 152 | 153 | ### An easter egg 154 | 155 | We can specify custom behavior by special casing search conditions and adding new side effects. In this block, we hijack the login process when the username is "dnedry". Instead of displaying the typical "login failed" message, we give the user a surprise. 156 | 157 | ``` 158 | search @browser @session 159 | app = [#app] 160 | 161 | [#form name: "Login" submission: [username password]] 162 | username = "dnedry" 163 | not([#user username password]) 164 | 165 | commit @browser 166 | app.children := none 167 | app.class -= "app-wrapper" 168 | app.class += "uh-uh-uh" 169 | ``` 170 | 171 | Clicking anywhere returns to the login screen 172 | 173 | ``` 174 | search @browser @session @event 175 | [#click] 176 | app = [#app class: "uh-uh-uh"] 177 | 178 | commit @browser 179 | app.class += "app-wrapper" 180 | app.class -= "uh-uh-uh" 181 | app.children += [#div sort: 0 text: "Jurassic Park System Security Interface"] 182 | app.page := "login" 183 | ``` 184 | 185 | ## A Custom Form Element 186 | 187 | Forms have a title and one or more sections. Each section has an optional name, and contains one or more fields. Each field additionally has the input type of that field (input, radio button, drop down list, etc.). 188 | 189 | A form starts as a `#form` record. 190 | 191 | ```eve 192 | search @browser 193 | form = [#form] 194 | 195 | bind @browser 196 | form += #div 197 | form.sort := 0 198 | form.class := "form" 199 | form.submission := [] 200 | ``` 201 | 202 | Display the form name 203 | 204 | ```eve 205 | search @browser 206 | form = [#form] 207 | 208 | bind @browser 209 | form.children += [#div children: 210 | [#h1 class: "form-name" sort: 0 text: form.name] 211 | [#div #form-message form sort: 1 class: "form-message"]] 212 | 213 | ``` 214 | 215 | Display each section. To properly display sections, we need to add them to the children of the form. 216 | 217 | ```eve 218 | search @browser 219 | form = [#form sections] 220 | 221 | bind @browser 222 | form.children += [#div form section: sections class: "form-section" sort: 1] 223 | sections.form := form 224 | ``` 225 | 226 | If the section has a name, display it 227 | 228 | ```eve 229 | search @browser 230 | section-display = [#div section] 231 | 232 | bind @browser 233 | section-display.children += [#h2 class: "section-name" text: section.name, sort: 0] 234 | ``` 235 | 236 | Display the fields in each section. As we did with sections, to display fields we need to move them over to the children of the section display. 237 | 238 | ```eve 239 | search @browser 240 | section-display = [#div section] 241 | field = section.fields 242 | 243 | bind @browser 244 | section-display.children += 245 | [#div field sort: field.sort form: section.form sort: 1 children: 246 | [tag: field.type, placeholder: field.field, class: field.type]] 247 | ``` 248 | 249 | Display a submit button at the end of the form 250 | 251 | ```eve 252 | search @browser 253 | form = [#form] 254 | 255 | bind @browser 256 | form.children += [#button #submit form sort: 100 text: "Submit"] 257 | ``` 258 | 259 | Forms can have an optional reset button, which clears the fields in the form 260 | 261 | ```eve 262 | search @browser 263 | form = [#form options: [reset: true]] 264 | 265 | bind @browser 266 | form.children += [#button #reset form sort: 101 text: "Reset"] 267 | ``` 268 | 269 | Clicking the reset button clears each field in the form 270 | 271 | ```eve 272 | search @event @browser 273 | [#click element: [#reset]] 274 | field-container = [#div field form] 275 | 276 | commit @browser 277 | field-container.children.value := none 278 | ``` 279 | 280 | ### Save Input to Records 281 | 282 | Form values are saved as a `#submission` when the submit button is clicked. This submission has a lifetime equal to that of the `#click`, so a submission must be committed to a record by the user. This allows the user to implement custom handling logic. 283 | 284 | One thing this form component does not handle is form validation. In a future example, we will demonstrate how certain fields can be required, while others are optional. This form will submit any fields that are filled, while omitting any that are not. If a form is handled expecting fields that aren't submitted, then the submission will simply be ignored. 285 | 286 | ```eve 287 | search @browser @event @session 288 | click = [#click element: [#submit form]] 289 | form = [#form submission] 290 | field-container = [#div field form] 291 | value = field-container.children.value 292 | key = field.field 293 | [#time timestamp: time] 294 | 295 | bind @browser 296 | // When used in a bind or commit. lookup[] creates a record with the give attribute and value. We use it here to create a record with the attribute as the field name. 297 | //For example, a login form with "username" and "password" fields could be accessed as [#form name: "Login" submission: [username password]] 298 | lookup[record: submission, attribute: key, value] 299 | field-container.children.value := none 300 | ``` 301 | 302 | ### Custom Input Types 303 | 304 | Render password fields 305 | 306 | ```eve 307 | search @browser 308 | password = [#password] 309 | 310 | bind @browser 311 | password += #input 312 | password.type := "password" 313 | password.class := "password" 314 | ``` 315 | 316 | Render custom button styles 317 | 318 | ```eve 319 | search @browser 320 | button = [#button] 321 | 322 | bind @browser 323 | button.class += "button" 324 | ``` 325 | 326 | ## Appendix 327 | 328 | ### Test Data 329 | 330 | ``` 331 | commit 332 | [#user username: "jhammond" name: "John Hammond" department: "Executive" password: "password"] 333 | [#user username: "dnedry" name: "Dennis Nedry" department: "Engineering" password: "Mr. Goodbytes"] 334 | [#user username: "hwu" name: "Henry Wu" department: "Genetics" password: "slartibartfast"] 335 | ``` 336 | 337 | ### Styles 338 | 339 | 340 | ```css 341 | {there is currently a bug that causes the first CSS block in an Eve program to be disregarded, so for a good time, leave this here} 342 | ``` 343 | 344 | 345 | ```css 346 | .application-container { 347 | background-color: #000; 348 | color: green; 349 | font-family: monospace; 350 | } 351 | 352 | @media (min-width: 1800px) { 353 | .app-wrapper { 354 | background-image: url(http://i.imgur.com/BBPkd29.gif); 355 | background-repeat: no-repeat; 356 | width: 800; 357 | height: 690px; 358 | background-size: 100% 100%; 359 | padding: 180px; 360 | padding-top: 130px; 361 | } 362 | } 363 | 364 | .profile { 365 | border: 1px solid green; 366 | padding: 10px; 367 | font-size:18px; 368 | border-radius: 5px; 369 | } 370 | 371 | .profile div { 372 | padding: 10px; 373 | } 374 | 375 | .form-section { 376 | border: 1px solid green; 377 | border-radius: 5px; 378 | padding: 10px; 379 | margin: 10px; 380 | } 381 | 382 | .form { 383 | border: 1px solid green; 384 | border-radius: 5px; 385 | padding: 10px; 386 | margin: 10px; 387 | color: green; 388 | } 389 | 390 | .input { 391 | background-color: #000; 392 | border-radius: 5px; 393 | border: 1px solid green; 394 | padding: 5px; 395 | margin: 5px; 396 | font-family: monospace; 397 | color: green; 398 | outline: none; 399 | } 400 | 401 | .password { 402 | background-color: #000; 403 | border-radius: 5px; 404 | border: 1px solid green; 405 | padding: 5px; 406 | margin: 5px; 407 | font-family: monospace; 408 | outline: none; 409 | color: green; 410 | } 411 | 412 | .button { 413 | background-color: #000; 414 | color: green; 415 | border-radius: 5px; 416 | border: 1px solid green; 417 | padding: 5px; 418 | margin: 5px; 419 | cursor: pointer; 420 | } 421 | 422 | .form-message { 423 | color: green; 424 | } 425 | 426 | .form-name { 427 | margin: 0px; 428 | } 429 | 430 | .section-name { 431 | margin: 0px; 432 | } 433 | 434 | .uh-uh-uh { 435 | width: 320px; 436 | height: 520px; 437 | background-image: url(http://i.imgur.com/yz53s4N.gif); 438 | background-color: #FFF; 439 | } 440 | ``` 441 | -------------------------------------------------------------------------------- /EX4-VoterRegistration.eve: -------------------------------------------------------------------------------- 1 | # New New York Voting Registration 2 | 3 | ## What is this? 4 | 5 | This example demonstrates how to capture input from forms and create and explore records from that. It should also provide a look at some of the particular complications and pitfalls in dealing with forms. In the future it'll most likely be useful to address a more complicated version of forms, but for now this is a good starting point to develop an intuition of the concepts. If anything seems oversimplified or susceptible to edge cases and user error, it's most likely from the effort to avoid bugs and overly complicated layouts. 6 | 7 | ## Page Layout 8 | 9 | ### Containers 10 | 11 | This one's pretty simple; I want a nav bar at the top to let me manually go to the different pages, and an app window where those pages are rendered. 12 | 13 | ``` 14 | commit @browser 15 | [#div class:"nav-bar"] 16 | [#div class:"app-window"] 17 | ``` 18 | 19 | ### Pages 20 | 21 | For every page I want, I make a record with an attribute of `target` to specify which each page is called. 22 | 23 | ``` 24 | commit 25 | [#page target:"Start"] 26 | [#page target:"Register"] 27 | [#page target:"Summary"] 28 | ``` 29 | 30 | ### Nav Bar Buttons 31 | 32 | By abstracting this step out, I can define all the pages I want as in the block before, and then here find those pages and create a button on the nav bar for each of them. 33 | 34 | ``` 35 | search @session @browser 36 | nav-bar = [#div class:"nav-bar"] 37 | [#page target] 38 | 39 | bind @browser 40 | nav-bar <- [children: [#button target text:target]] 41 | ``` 42 | 43 | This partner block works with the nav buttons by simply setting the app window to whichever nav button is clicked. 44 | 45 | ``` 46 | search @session @browser @event 47 | [#click element: [#button target]] 48 | window = [#app-window] 49 | 50 | commit 51 | window.target := target 52 | ``` 53 | 54 | ### Starting Page 55 | 56 | This commits a record called `#app-window` whose attribute `target` specifies which page gets rendered. In this case, when the app starts up, I want the landing view to be the Start page. 57 | 58 | ``` 59 | commit 60 | [#app-window target: "Start"] 61 | 62 | ``` 63 | ## Start Page 64 | 65 | The starting page isn't very complicated - it contains a thank you message for the civically engaged Earthicans, the flag of Earth, a button to start a new registration for another voter, and a subtle sponsorship message. 66 | 67 | ``` 68 | search @session @browser 69 | [#app-window target:"Start"] 70 | window = [#div class:"app-window"] 71 | 72 | bind @browser 73 | window.class += "home-screen" 74 | window <- [children: 75 | [#div class:"thank-you" text:"Thank you for registering to vote!"] 76 | [#img class:"old-freebie" src:"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Futurama_flag_of_Earth.svg/720px-Futurama_flag_of_Earth.svg.png"] 77 | [#button target:"Register" class:"begin-btn" text:"Begin a new registration"] 78 | [#div class:"sponsor" text:"This election sponsored with love by MomCorp"] 79 | ] 80 | ``` 81 | 82 | ## Voter Registration Page 83 | 84 | The voter registration page starts with questions that determine the eligibility of a registrant to complete the form. A voter must be both a citizen, and not convicted of a felony. This block sets up the form, and posses those two questions. 85 | 86 | ``` 87 | search @session @browser 88 | [#app-window target:"Register"] 89 | window = [#div class:"app-window"] 90 | 91 | bind @browser 92 | window.class += "voter-registration" 93 | window <- [children: 94 | [#form name: "Voter Registration" sections: 95 | 96 | [#section sort: 1 name: "Qualifications" fields: 97 | [#field #required sort: 1 type: "radio" label: "Are you a citizen?" field: "citizenship" options: 98 | [label: "Yes"] 99 | [label: "No"]] 100 | [#field #required sort: 2 type: "radio" label: "Have you been convicted of a felony?" field: "felony" options: 101 | [label: "Yes"] 102 | [label: "No"]]]]] 103 | ``` 104 | 105 | If either of the qualifications are not met in the submission, then a message is displayed informing the registrant. We can access the from submission through a `#submission` record. The attributes of the submission map to the field attribute in each of the `#field` records of the form. Form submissions can be either `#valid`, meaning the submission contains all of the required fields; or `#invalid`, meaning the submission is missing some of the required fields. This gives you flexibility on how to handle form submissions. 106 | 107 | ``` 108 | search @browser @session 109 | [#submission #valid form citizenship felony] 110 | form = [#form name: "Voter Registration"] 111 | not(form = [#qualified]) 112 | 113 | disqualified = if citizenship != "Yes" then "disqualified" 114 | else if felony != "No" then "disqualified" 115 | 116 | form-message = [#form-message form] 117 | 118 | bind @browser 119 | form-message.children += [#div class: "error-message" text: "Sorry, you are not qualified to register to vote"] 120 | ``` 121 | 122 | The form is `#qualified` if the qualifications are met. 123 | 124 | ``` 125 | search @browser @session 126 | [#submission #valid form citizenship: "Yes" felony: "No"] 127 | form = [#form name: "Voter Registration"] 128 | 129 | bind @browser 130 | form += #qualified 131 | ``` 132 | 133 | When the registrant is deemed qualified, the rest of the voter registration form is revealed. This is done by attaching additional sections to the already exiting form, defined above. 134 | 135 | ```eve 136 | search @session @browser 137 | [#app-window target: "Register"] 138 | window = [#div class:"app-window"] 139 | [#party party-name priority] 140 | [#gender gender-name order] 141 | registration-form = [#form #qualified name: "Voter Registration"] 142 | 143 | bind @browser 144 | registration-form.sections += ( 145 | [#section sort: 2 name: "Personal Information" fields: 146 | [#field #required sort: 1 type: "input" label: "First Name" field: "first-name"] 147 | [#field sort: 2 type: "input" label: "Middle Name" field: "middle-name"] 148 | [#field #required sort: 3 type: "input" label: "Last Name" field: "last-name"] 149 | [#field #required sort: 4 type: "input" label: "Birthday" field: "birthday" placeholder:"MM-DD-YYYY"] 150 | [#field #required sort: 5 type: "radio" label: "Gender" field: "gender" options: 151 | [label: gender-name]] 152 | [#field sort: 6 type: "input" label:"Phone Number" field: "phone" placeholder:"XXX-XXX-XXXX"] 153 | [#field #required sort: 7 type: "input" label:"Last 4 Digits of SSN" field: "ssn" placeholder:"XXXX"] 154 | ] 155 | 156 | [#section sort: 3 name: "Address" fields: 157 | [#field #required sort: 1 type: "input" label: "Address Line 1" field: "address1"] 158 | [#field sort: 2 type: "input" label: "Address Line 2" field: "address2"] 159 | [#field #required sort: 3 type: "input" label: "City" field: "city"] 160 | [#field #required sort: 4 type: "input" label: "State" field: "state"] 161 | [#field #required sort: 5 type: "input" label: "Zip" field: "zip"] 162 | ] 163 | 164 | [#section sort: 4 name: "Political Affiliation" fields: 165 | [#field #required sort: 1 type: "radio" label: "Of which party are you a member?" field: "party" options: 166 | [label: party-name]]]) 167 | ``` 168 | 169 | This listens for a new submission on the registration form, and creates a new #voter record with the submitted information. 170 | 171 | ```eve 172 | search @browser @session 173 | form = [#form #qualified] 174 | submission = [#valid form first-name middle-name last-name birthday gender phone ssn address1 address2 city state zip party] 175 | window = [#app-window] 176 | 177 | // Reset the form for a new submission 178 | commit @browser 179 | form -= #qualified 180 | form += #reset 181 | 182 | // Save the submission as a new voter record, and change the page to a submission summary page 183 | commit @session 184 | window.target := "Submission" 185 | window.voter := voter 186 | voter = [#voter first-name middle-name last-name birthday ssn phone gender party address: [address1 address2 city state zip]] 187 | ``` 188 | 189 | The registration summary page displays the voter's new voter card, as and contains a button that redirects back to the starting page. 190 | 191 | ``` 192 | search @session @browser 193 | [#app-window target: "Submission" voter] 194 | window = [#div class: "app-window"] 195 | 196 | bind @browser 197 | window.children += [#div children: 198 | [#h1 text: "Registration Summary"] 199 | [#voter-card voter] 200 | [#button target: "Start" class: "submit-btn" text: "Finish"]] 201 | ``` 202 | 203 | 204 | ### Gender options 205 | 206 | Technically this question susses out both one's gender and organic life versus robot status, and the end result is the need for a list of genders rather than a binary option. Instead of adding these manually in the voter registration page layout, I've made a list here, open to any additional genders that need to be added. Any gender on this list will get added to the voter registration page, which gets sorted by the `order` attribute. 207 | 208 | ``` 209 | commit 210 | [#gender gender-name:"Male" order:1] 211 | [#gender gender-name:"Female" order:2] 212 | [#gender gender-name:"Manbot" order:3] 213 | [#gender gender-name:"Fembot" order:4] 214 | ``` 215 | 216 | ### Political Party Options 217 | 218 | While the Tastycrats and Fingerlicans are the primary parties, there are many third parties to choose from, and they all get listed here. Again, this makes it easy to add or remove political parties without having to mess around with the voter registration page. 219 | 220 | ``` 221 | commit 222 | [#party party-name:"Tastycrat Party" priority:1] 223 | [#party party-name:"Fingerlican Party" priority:2] 224 | [#party party-name:"One Cell, One Vote" priority:3] 225 | [#party party-name:"Green Party" priority:4] 226 | [#party party-name:"Brain Slug Party" priority:5] 227 | [#party party-name:"Dudes for the Legalization of Hemp" priority:6] 228 | [#party party-name:"Bull Space Moose Party" priority:7] 229 | [#party party-name:"National Ray-Gun Association" priority:8] 230 | [#party party-name:"People for the Ethical Treatment of Humans" priority:9] 231 | [#party party-name:"Voter Apathy Party" priority:10] 232 | [#party party-name:"Rainbow Whigs" priority:11] 233 | [#party party-name:"Antisocialists" priority:12] 234 | [#party party-name:"Reform Party" priority:13] 235 | [#party party-name:"No Party" priority:14] 236 | ``` 237 | 238 | ### Summary Page 239 | 240 | This page gives an administrator an overview of all the Earthicans who have registered to vote, and drawing the contents is actually pretty easy. All we need is a `#voter-card` for each `#voter` record. The layout for what the `#voter-card` looks like gets handled in the next block, and the brunt of the work for this section is actually the styling. 241 | 242 | ```eve 243 | search @session @browser 244 | [#app-window target:"Summary"] 245 | window = [#div class:"app-window"] 246 | voter = [#voter] 247 | 248 | bind @browser 249 | window.children += [#voter-card voter] 250 | ``` 251 | 252 | ### Voter Cards 253 | 254 | Each voter gets a `#voter-card` in the browser, which displays the voter's information. 255 | 256 | ``` 257 | search @session @browser 258 | voter-card = [#voter-card voter] 259 | voter = [#voter first-name last-name birthday gender ssn party address] 260 | phone = if voter.phone then "Phone: {{voter.phone}}" else "" 261 | address2 = if address.address2 then " {{address.address2}} " else " " 262 | middle-name = if voter.middle-name then " {{voter.middle-name}} " else " " 263 | 264 | bind @browser 265 | voter-card <- [#div class:"voter-card" children: 266 | [#div class:"voter-name" text: "{{first-name}}{{middle-name}}{{last-name}}"] 267 | [#div class:"birth-info" children: 268 | [#div class:"voter-address" text: "{{address.address1}}{{address2}}{{address.city}} {{address.state}} {{address.zip}}"] 269 | [#div class:"voter-birth" text:"Born: {{birthday}}"] 270 | [#div class:"voter-sex" text:"Gender: {{gender}}"] 271 | [#div class:"voter-phone" text:phone] 272 | [#div class:"voter-ssn" text:"SSN: XXX-XX-{{ssn}}"]] 273 | [#div class:"voter-party" text:"Affiliation: {{party}}"]] 274 | ``` 275 | 276 | ## Appendix 277 | 278 | ### Sample Data 279 | 280 | Here's a few sample registered voters to help populate the Summary page. 281 | 282 | ```eve 283 | commit 284 | [#voter 285 | first-name: "Philip" 286 | middle-name: "J." 287 | last-name: "Fry" 288 | birthday: "08-14-1974" 289 | gender: "male" 290 | ssn: "0810" 291 | party: "Voter Apathy Party" 292 | address: [address1: "620 W 42nd St" address2: "Apt 00100100" city: "New New York" state: "NY" zip: "10036"]] 293 | 294 | [#voter 295 | first-name: "Hubert" 296 | middle-name: "J." 297 | last-name: "Farnsworth" 298 | birthday: "04-09-2841" 299 | gender: "male" 300 | phone: "04-09-2841" 301 | ssn: "2458" 302 | party: "National Ray-Gun Association" 303 | address: [address1: "650 W 57th S." city: "New New York" state: "NY" zip: "10036"]] 304 | 305 | [#voter 306 | first-name: "Turanga" 307 | last-name: "Leela" 308 | birthday: "07-29-2975" 309 | gender: "Female" 310 | phone: "212-307-7760" 311 | ssn: "3001" 312 | party: "No party" 313 | address: [address1: "715 9th Ave" address2: "Apt 1I" city: "New New York" state: "NY" zip: "10036"]] 314 | 315 | [#voter 316 | first-name: "Bender" 317 | middle-name: "Bending" 318 | last-name: "Rodriguez" 319 | birthday: "01-02-2996" 320 | gender: "Manbot" 321 | party: "No party" 322 | ssn: "BU22" 323 | address: [address1: "620 W 42nd St" address2: "Apt 00100100" city: "New New York" state: "NY" zip: "10036"]] 324 | 325 | [#voter 326 | first-name: "John" 327 | middle-name: "A." 328 | last-name: "Zoidberg" 329 | birthday: "05-05-2914" 330 | gender: "Male" 331 | ssn: "DXC7" 332 | party: "People for the Ethical Treatment of Humans" 333 | address: [address1: "The dumpster behind 650 W 57th St" city: "New New York" state: "NY" zip: "10036"]] 334 | 335 | [#voter 336 | first-name: "Hermes" 337 | last-name: "Conrad" 338 | birthday: "07-15-2959" 339 | gender: "Male" 340 | ssn: "6789" 341 | party: "Brain Slug Party" 342 | address: [address1: "105 Duane St" address2: "Apt 3007" city: "New New York" state: "NY" zip: "10007"]] 343 | 344 | [#voter 345 | first-name: "Amy" 346 | last-name: "Wong" 347 | birthday: "05-04-2978" 348 | gender: "Female" 349 | phone: "212-995-8833" 350 | ssn: "5523" 351 | party: "Dudes for the Legalization of Hemp" 352 | address: [address1: "10 West St." city: "New New York" state: "NY" zip: "10007"]] 353 | ``` 354 | 355 | ### A Custom Form Element 356 | 357 | Forms have a title and one or more sections. Each section has an optional name, and contains one or more fields. Each field additionally has the input type of that field (input, radio button, drop down list, etc.). 358 | 359 | A form starts as a `#form` record. 360 | 361 | ```eve 362 | search @browser 363 | form = [#form] 364 | 365 | bind @browser 366 | form += #div 367 | form.sort := 0 368 | form.class := "form" 369 | ``` 370 | 371 | Display the form name 372 | 373 | ```eve 374 | search @browser 375 | form = [#form] 376 | 377 | bind @browser 378 | form.children += [#div children: 379 | [#h1 class: "form-name" sort: 0 text: form.name] 380 | [#div #form-message form sort: 1 class: "form-message"]] 381 | 382 | ``` 383 | 384 | Display each section. To properly display sections, we need to add them to the children of the form. 385 | 386 | ```eve 387 | search @browser 388 | form = [#form sections] 389 | 390 | bind @browser 391 | form.children += [#div form section: sections class: "form-section" sort: sections.sort] 392 | sections.form := form 393 | ``` 394 | 395 | If the section has a name, display it 396 | 397 | ```eve 398 | search @browser 399 | section-display = [#div section] 400 | 401 | bind @browser 402 | section-display.children += [#h2 class: "section-name" text: section.name, sort: 0] 403 | ``` 404 | 405 | Display the fields in each section. As we did with sections, to display fields we need to move them over to the children of the section display. 406 | 407 | ```eve 408 | search @browser 409 | section-display = [#div section] 410 | field = section.fields 411 | 412 | bind @browser 413 | field.form := section.form 414 | section-display.children += 415 | [#div #field field sort: field.sort form: section.form sort: 1] 416 | ``` 417 | 418 | Display a submit button at the end of the form 419 | 420 | ```eve 421 | search @browser 422 | form = [#form] 423 | 424 | bind @browser 425 | form.children += [#button #submit form sort: 100 text: "Submit"] 426 | ``` 427 | 428 | **Handle form submission** 429 | 430 | When the submit button is clicked, the form enters a #validating stage, where all form data are collected and compared against the field specifications. If any field does not match, then the submission is tagged #invalid and those fields are called out to the user to correct. 431 | 432 | If all fields are valid, then the submission is marked #valid and the data are stored in a new `#submission` record stamped with the submission time. 433 | 434 | We start by saving the data in the submission record, and mark the submission as `#validating`. Validation is covered in the next section. 435 | 436 | ``` 437 | search @event @browser @session 438 | [#click element: [#submit form]] 439 | [#field form field] 440 | [#time timestamp: time] 441 | 442 | value = if field.value then field.value 443 | else "" 444 | 445 | commit @browser 446 | submission = [#submission #validating form time] 447 | lookup[record: submission, attribute: field.field value] 448 | ``` 449 | 450 | **Handle form validation** 451 | 452 | If a field is `#required` and a submission` is `#validating`, then we check that the field is actually filled. If any required fields are not filled, then we mark the submission as `#invalid`. 453 | 454 | ``` 455 | search @browser 456 | submission = [#submission #validating form] 457 | field = [#field #required form not(value)] 458 | 459 | commit @browser 460 | submission -= #validating 461 | submission += #invalid 462 | ``` 463 | 464 | Give invalid fields a custom class. Also display a message at the top if any fields are required but missing. The bind here allows the form to be corrected dynamically, unmarking previously invalid fields as soon as they are filled. 465 | 466 | ``` 467 | search @browser 468 | [#submission #invalid form] 469 | field = [#field #required form not(value)] 470 | field-label = [#h3 field] 471 | form-message = [#form-message form] 472 | 473 | bind @browser 474 | field-label.class += "required-field" 475 | form-message.children += [#div class: "error-message" text: "Please fill out all required fields"] 476 | ``` 477 | 478 | If all required fields are valid, then the submission is marked `#valid` 479 | 480 | ``` 481 | search @browser 482 | submission = [#submission #validating form] 483 | not([#field #required form not(value)]) 484 | 485 | commit @browser 486 | submission -= #validating 487 | submission += #valid 488 | ``` 489 | 490 | **Reset the form** 491 | 492 | Resetting currently has a bug that affects multiple form submissions in succession. 493 | 494 | ``` 495 | search @browser 496 | form = [#form #reset] 497 | submission = [#submission form] 498 | 499 | commit @browser 500 | form -= #reset 501 | submission := none 502 | ``` 503 | 504 | **Custom Input Types** 505 | 506 | Custom input types are implemented by taking the value of the input (whatever that may be) and assigning it to the value attribute of its corresponding `#field` record. For example, the value of a text field is just `value`. However, the value of a radio field is the `value` of the option that is checked, so some additional logic needs to be in place to work with these various components. However, the interface to access these data from Eve is to uniformly look at the `value` on the `#field`. In this example, we implement both the radio and the input fields, but more can be implemented in this way. 507 | 508 | Render radio buttons 509 | 510 | ``` 511 | search @browser 512 | field-display = [#div #field field form] 513 | field.type = "radio" 514 | 515 | bind @browser 516 | field-display.children += [#div #radio field children: 517 | [#h3 field text: field.label] 518 | [#div option: field.options children: 519 | [#div class: "radio-label" text: field.options.label] 520 | [#input class: "radio-button" field form type: "radio" name: field.field value: field.options.label]]] 521 | ``` 522 | 523 | Save radio value in its respective `#field` 524 | 525 | ``` 526 | search @browser @session 527 | radio = [#input type: "radio" checked: true field value] 528 | field = [#field] 529 | 530 | bind @browser 531 | field.value := value 532 | 533 | ``` 534 | 535 | Render input fields 536 | 537 | ``` 538 | search @browser 539 | field-display = [#div #field field form] 540 | field.type = "input" 541 | placeholder = if field.placeholder then field.placeholder 542 | else "" 543 | 544 | bind @browser 545 | field-display.children += [#div children: 546 | [#h3 field text: field.label] 547 | [#input class: "text-input" type: "input" form field name: field.field placeholder]] 548 | ``` 549 | 550 | Save input value to respective `#field` 551 | 552 | ``` 553 | 554 | search @browser @session 555 | input = [#input type: "input" field value] 556 | field = [#field] 557 | 558 | bind @browser 559 | field.value := value 560 | ``` 561 | 562 | Render password fields 563 | 564 | ```eve 565 | search @browser 566 | password = [#password] 567 | 568 | bind @browser 569 | password += #input 570 | password.type := "password" 571 | password.class := "password" 572 | ``` 573 | 574 | Render custom button styles 575 | 576 | ```eve 577 | search @browser 578 | button = [#button form] 579 | 580 | bind @browser 581 | button.class += "submit-btn" 582 | ``` 583 | 584 | ### Styles 585 | 586 | This app needs a good amount of CSS to style the layout of the registration form and the voter cards. 587 | 588 | ```css 589 | {for a good time, leave this here} 590 | ``` 591 | 592 | ```css 593 | .radio-label { 594 | float: left; 595 | font-size: 13px; 596 | width: 300px; 597 | } 598 | 599 | .radio-button { 600 | width: 100px; 601 | } 602 | 603 | .text-input { 604 | width: 350px; 605 | border-radius: 5px; 606 | padding: 5px; 607 | } 608 | 609 | .error-message { 610 | background-color: #FA8072; 611 | padding: 10px; 612 | color: white; 613 | font-size: 16px; 614 | } 615 | 616 | .required-field { 617 | color: red; 618 | } 619 | ``` 620 | 621 | ```css 622 | @font-face { 623 | font-family: "futurama"; 624 | src: url("assets/fr-bold.ttf") format("truetype"); 625 | } 626 | 627 | .nav-bar { 628 | order: 1; 629 | display: flex; 630 | flex-direction: row; 631 | margin-bottom: 10px; 632 | min-height: 44px; 633 | } 634 | 635 | .nav-bar button { 636 | padding: 10px; 637 | margin-right: 15px; 638 | border: 1px solid #555; 639 | border-radius: 5px; 640 | background: none; 641 | font-size: 14px; 642 | cursor: pointer; 643 | } 644 | 645 | .app-window.home-screen { 646 | background: radial-gradient(circle at bottom,#5ef5fc,#0b527a,#012535); 647 | position: relative; 648 | display: flex; 649 | flex-direction: column; 650 | align-items: center; 651 | height: 100%; 652 | min-height: 1000px; 653 | order: 2; 654 | } 655 | 656 | .thank-you { 657 | font-family: "futurama"; 658 | text-align: center; 659 | margin: 100px 0px; 660 | font-size: 48px; 661 | color: white; 662 | } 663 | 664 | .begin-btn { 665 | width: auto; 666 | flex: 0 0 50px; 667 | padding: 0px 20px; 668 | margin-top: 5vh; 669 | font-size: 16px; 670 | background: white; 671 | border: 1px solid #555; 672 | border-radius: 6px; 673 | text-transform: uppercase; 674 | cursor: pointer; 675 | } 676 | 677 | .sponsor { 678 | background: url(http://i.imgur.com/sWkxxNU.png) no-repeat; 679 | background-size: 60px; 680 | background-position: bottom center; 681 | width: auto; 682 | height: 100px; 683 | color: white; 684 | position: absolute; 685 | bottom: 30px; 686 | text-transform: uppercase; 687 | font-size: 24px; 688 | text-align: center; 689 | } 690 | 691 | .app-window { 692 | background: white; 693 | order: 2; 694 | display: flex; 695 | flex-direction: column; 696 | overflow: scroll; 697 | height: 100%; 698 | } 699 | 700 | .voter-registration > div { 701 | display: flex; 702 | border-top: 1px solid #0b527a; 703 | align-items: center; 704 | padding: 15px 0px; 705 | flex: 0 0 auto; 706 | } 707 | 708 | .voter-registration h1 { 709 | font-size: 80px; 710 | width: 150px; 711 | margin-right: 50px; 712 | padding-left: 20px; 713 | color: #0b527a; 714 | } 715 | 716 | .voter-registration input { 717 | margin-left: 8px; 718 | } 719 | 720 | .name-field { 721 | width: 300px; 722 | display: inline; 723 | } 724 | 725 | .address-field { 726 | width: 500px; 727 | display: inline; 728 | } 729 | 730 | .radio-field { 731 | margin: 6px 0px; 732 | } 733 | 734 | .gender-field { 735 | display: flex; 736 | align-items: center; 737 | } 738 | 739 | .gender-field div { 740 | margin-right: 15px; 741 | } 742 | 743 | .voter-card { 744 | flex: 0 0 auto; 745 | display: flex; 746 | flex-direction: column; 747 | margin-bottom: 10px; 748 | border: 1px solid #555; 749 | border-radius: 6px; 750 | padding: 10px; 751 | } 752 | 753 | .submit-btn { 754 | width: auto; 755 | height: 60px; 756 | margin-top: 20px; 757 | padding: 0px 60px; 758 | font-size: 16px; 759 | color: white; 760 | background: #0b527a; 761 | border: 1px solid #555; 762 | border-radius: 6px; 763 | text-transform: uppercase; 764 | align-self: center; 765 | flex: 0 0 auto; 766 | } 767 | 768 | .voter-name { 769 | font-size: 18px; 770 | margin-bottom: 10px; 771 | font-weight: 600; 772 | } 773 | 774 | .voter-address { 775 | font-size: 18px; 776 | margin-bottom: 10px; 777 | } 778 | 779 | .birth-info { 780 | display: flex; 781 | } 782 | 783 | .birth-info div { 784 | padding-right: 50px; 785 | } 786 | 787 | @media (max-width:2275px) { 788 | 789 | .nav-bar { 790 | min-height: 25px; 791 | } 792 | 793 | .nav-bar button { 794 | padding: 0px 10px; 795 | margin-right: 10px; 796 | font-size: 10px; 797 | } 798 | 799 | .app-window.home-screen { 800 | min-height: 550px; 801 | } 802 | 803 | .thank-you { 804 | text-align: center; 805 | margin: 50px 0px; 806 | font-size: 26px; 807 | } 808 | 809 | .old-freebie { 810 | width: 80%; 811 | } 812 | 813 | .begin-btn { 814 | flex: 0 0 40px; 815 | padding: 0px 20px; 816 | margin-top: 50px; 817 | font-size: 14px; 818 | } 819 | 820 | .sponsor { 821 | background-size: 40px; 822 | width: auto; 823 | height: 70px; 824 | bottom: 20px; 825 | font-size: 14px; 826 | } 827 | 828 | .app-window { 829 | font-size: 12px; 830 | } 831 | 832 | .voter-registration > div { 833 | display: flex; 834 | flex-direction: column; 835 | align-items: center; 836 | padding: 0px 10px 10px 10px; 837 | flex: 0 0 auto; 838 | min-width: 192px; 839 | } 840 | 841 | .voter-registration > div > div{ 842 | width: 100%; 843 | } 844 | 845 | .voter-registration h1 { 846 | font-size: 30px; 847 | flex: 0 0 20px; 848 | width: auto; 849 | margin-right: 0px; 850 | padding-left: 0px; 851 | } 852 | 853 | .radio-field input { 854 | margin-left: 6px; 855 | } 856 | 857 | .name-field { 858 | width: 100%; 859 | display: flex; 860 | } 861 | 862 | .birth-field { 863 | width: 100%; 864 | display: flex; 865 | } 866 | 867 | .phone-field { 868 | width: 100%; 869 | display: flex; 870 | } 871 | 872 | .voter-registration .address-field { 873 | width: 100%; 874 | display: inline; 875 | margin-left: 0px; 876 | } 877 | 878 | .voter-registration .ssn-field { 879 | margin-left: 0px; 880 | width: 100%; 881 | display: flex; 882 | } 883 | 884 | .radio-field { 885 | margin: 5px 0px; 886 | } 887 | 888 | .submit-btn { 889 | width: auto; 890 | height: 40px; 891 | margin: 25px 0px; 892 | padding: 0px 40px; 893 | font-size: 14px; 894 | cursor: pointer; 895 | } 896 | 897 | .political-party .radio-field { 898 | display: flex; 899 | flex-direction: column; 900 | margin-bottom: 10px; 901 | } 902 | 903 | .political-party input { 904 | margin-left: 0px; 905 | } 906 | 907 | .voter-name { 908 | font-size: 16px; 909 | } 910 | 911 | .voter-address { 912 | font-size: 16px; 913 | } 914 | 915 | .birth-info { 916 | flex-direction: column; 917 | } 918 | 919 | .birth-info div { 920 | padding-right: 0px; 921 | } 922 | 923 | } 924 | ``` 925 | -------------------------------------------------------------------------------- /EX5-BattleSchool.eve: -------------------------------------------------------------------------------- 1 | # Battle School 2 | ```css 3 | {for a good time, leave this here} 4 | ``` 5 | 6 | ## Page Layout 7 | ### Containers 8 | This one's pretty simple; I want a nav bar at the top to let me manually go to the different pages, and an app window where those pages are rendered. 9 | 10 | ``` 11 | commit @browser 12 | [#div class:"nav-bar"] 13 | [#div class:"app-window"] 14 | ``` 15 | 16 | ### Pages 17 | For every page I want, I make a record with an attribute of `target` to specify which each page is called. 18 | 19 | ``` 20 | commit 21 | [#page target:"Broadcast"] 22 | [#page target:"Control"] 23 | ``` 24 | 25 | ### Nav Bar Buttons 26 | By abstracting this step out, I can define all the pages I want as in the block before, and then here find those pages and create a button on the nav bar for each of them. 27 | 28 | ``` 29 | search @session @browser 30 | nav-bar = [#div class:"nav-bar"] 31 | [#page target] 32 | 33 | bind @browser 34 | nav-bar <- [children: [#button target text:target]] 35 | ``` 36 | 37 | This partner block works with the nav buttons by simply setting the app window to whichever nav button is clicked. 38 | 39 | ``` 40 | search @session @browser @event 41 | [#click element: [#button target]] 42 | window = [#app-window] 43 | 44 | commit 45 | window.target := target 46 | ``` 47 | 48 | ### Starting Page 49 | This commits a record called #`app-window` whose attribute `target` specifies which page gets rendered. In this case, when the app starts up, I want the landing view to be the Broadcast page. 50 | 51 | ``` 52 | commit 53 | [#app-window target:"Broadcast"] 54 | ``` 55 | 56 | ### Sci-Fi Font 57 | It's not the future if there's not futuristic-looking text. 58 | 59 | ``` 60 | commit @browser 61 | [#link href:"https://fonts.googleapis.com/css?family=Orbitron|Play" rel:"stylesheet"] 62 | ``` 63 | 64 | ## Broadcast 65 | ### Drawing the Page 66 | The Broadcast page is meant to serve much like the departures or arrivals screen at an airport - it is purely an informative screen showing which armies have been scheduled in which rooms. There are 9 total battle rooms, so I use the `range` function to generate 9 rooms, each of which has two sides labeled A and B. There's also a #`versus` block whose text will change based on whether or not there are armies scheduled to that room. 67 | 68 | ``` 69 | search @session @browser 70 | [#app-window target:"Broadcast"] 71 | window = [#div class:"app-window"] 72 | i = range[from: 1, to: 9] 73 | 74 | bind @browser 75 | window.class += "broadcast" 76 | window.children += [#div i class:"battle-room" children: 77 | [#div class:"room-title" text:"{{i}}"] 78 | [#div class:"matchup" children: 79 | [#room class:"teamA" room:i side:"A"] 80 | [#versus room:i] 81 | [#room class:"teamB" room:i side:"B"] 82 | ] 83 | ] 84 | ``` 85 | 86 | ### Drawing Upcoming Battles 87 | This block checks to see if there's an army assigned to a room and will inject a card for that army into its room slot, but only if there's another army that has been assigned to the opposing side in that room. Having only an A side or a B side will fail the search. If both sides are accounted for however, the search will pass for both of those armies, and so both cards will get injected. 88 | 89 | ```eve 90 | search @session @browser 91 | slot = [#room room side] 92 | army = [#army room side name color1 color2 color3 uniform] 93 | other-side = if side = "A" then "B" 94 | else if side = "B" then "A" 95 | [#army room side:other-side] 96 | versus = [#versus room] 97 | 98 | bind @browser 99 | slot.class += ("army-card") 100 | slot <- [#div name children: 101 | [#div class:"army-name" text:name] 102 | [#div class:"ribbon" children: 103 | [#div class:color1] 104 | [#div class:color2] 105 | [#div class:color3]] 106 | ] 107 | ``` 108 | 109 | If a room has two armies assigned to it, one to each side, then the #`versus` block becomes just the text "VS" between the two army cards. 110 | 111 | ``` 112 | search @session @browser 113 | versus = [#versus room] 114 | [#army room side:"A"] 115 | [#army room side:"B"] 116 | 117 | bind @browser 118 | versus <- [#div class:"versus-text" text:"VS"] 119 | ``` 120 | 121 | If a room is missing either an A or a B side, or both, I want a message to be displayed saying that a battle hasn't been scheduled yet. Since this comprises a union in set theory, I need two blocks to explicity handle all my possibilities. This first one looks to see if both sides have yet to be assigned, and injects the message into the #`versus` block if that happens to be the case. 122 | 123 | ``` 124 | search @session @browser 125 | versus = [#versus room] 126 | not([#army room side:"A"]) 127 | not([#army room side:"B"]) 128 | 129 | bind @browser 130 | versus <- [#div class:"empty-room" text:"No battle scheduled"] 131 | ``` 132 | 133 | This second block gives us the other half of the union by checking to see if either an A side or a B side has been assigned, but not its complement, and will inject the same message into the #`versus` block. 134 | 135 | ``` 136 | search @session @browser 137 | versus = [#versus room] 138 | army = [#army room side] 139 | other-side = if side = "A" then "B" 140 | else if side = "B" then "A" 141 | not([#army room side:other-side]) 142 | 143 | bind @browser 144 | versus <- [#div class:"empty-room" text:"No battle scheduled"] 145 | ``` 146 | 147 | ## Control 148 | ### Drawing the Page 149 | The Control page is laid out to be displayed on a smaller device - perhaps a smart phone or a tablet - and is used to assign which armies will fight in which rooms. Again, for the nine rooms, the range function is used to identify and number them, and each room is given two sides. A list of all the available armies - that is, those which have not been assigned to a room - is also drawn. 150 | 151 | ``` 152 | search @session @browser 153 | [#app-window target:"Control"] 154 | [#army name color1 color2 color3 uniform not(room)] 155 | i = range[from: 1, to: 9] 156 | window = [#div class:"app-window"] 157 | 158 | bind @browser 159 | window.class += "control" 160 | window.children += ([#img class:"logo" src:"http://vignette2.wikia.nocookie.net/ansible/images/6/69/InternationalFleetLogo.png"], 161 | [#div class:"control-lists" children: 162 | [#div class:"all-rooms" children: 163 | [#div class:"room" i children: 164 | [#div class:"title" text:"Battle Room {{i}}"] 165 | [#div room:i side:"A" class:"battle-slot"] 166 | [#div class:"vs-line" text:"vs"] 167 | [#div room:i side:"B" class:"battle-slot"] 168 | ] 169 | ], 170 | [#div class:"all-armies" children: 171 | [#div class:"title" text:"Armies"] 172 | [#div sort:name name class:("army-tab", uniform) text:name] 173 | ] 174 | ]) 175 | ``` 176 | 177 | As an addition to main drawing block for the Control page, this block checks to see if there are any armies assigned to a slot in a battle room. If it is, that army has its tab drawn in the corresponding slot in the browser. 178 | 179 | ``` 180 | search @session @browser 181 | window = [#div class:"battle-slot" room side] 182 | [#army name uniform room side] 183 | 184 | bind @browser 185 | window.children += [#div class:("army-tab", uniform) text:name] 186 | ``` 187 | 188 | ### Selecting 189 | In order to be able to assign armies to slots, there needs to be a mechanism to select both elements. I've chosen to demonstrate selection in two different ways to cover some of the possibilities of how this might be achieved, and because armies each have a record stored in the session database, I used them to show selection by modifying a session record. The intended workflow here is to click an army to highlight it, then choose a slot to assign them to, or go the other way around and click a battle slot to highlight it, then choose an army to assign there. That means I only want to highlight an element if there's nothing else already selected - otherwise, I'm probably trying to assign an army. This block searches for a click on an army tab and, as long as that army isn't already highlighted, nor any battle slots, adds the #`highlighted` tag to the record of the clicked army. 190 | 191 | ``` 192 | search @session @event @browser 193 | [#click element: [#div class:"army-tab" name]] 194 | army = [#army name not(#highlighted)] 195 | not([#div #highlighted class:"battle-slot" room]) 196 | 197 | commit 198 | army += #highlighted 199 | ``` 200 | 201 | This block adds the #`highlighted` tag to a record in browser instead of session. If a battle slot gets clicked and isn't already highlighted and doesn't have an army already assigned to it, then that battle slot becomes highlighted. 202 | 203 | ``` 204 | search @session @event @browser 205 | battle-slot = [#div class:"battle-slot" room side] 206 | [#click element: battle-slot] 207 | not([#div #highlighted class:"battle-slot" room side]) 208 | not([#click element:[#div class:"army-tab"]]) 209 | not([#army #highlighted]) 210 | 211 | commit @browser 212 | battle-slot += #highlighted 213 | ``` 214 | 215 | ### Assigning 216 | Once an army is #`highlighted`, if a battle slot gets clicked, the highlighted army gets assigned to that particular slot, which corresponds to both a `room` and a `side`. 217 | 218 | ``` 219 | search @session @event @browser 220 | army = [#army #highlighted] 221 | [#click element: [#div class:"battle-slot" room side]] 222 | 223 | commit 224 | army.room := room 225 | army.side := side 226 | ``` 227 | 228 | On the flip side, if a battle slot is #`highlighted` and an army gets clicked, the clicked army gets assigned to the highlighted slot. 229 | 230 | ``` 231 | search @session @event @browser 232 | [#div #highlighted class:"battle-slot" room side] 233 | army = [#army name] 234 | [#click element: [#div class:"army-tab" name]] 235 | 236 | commit 237 | army.room := room 238 | army.side := side 239 | ``` 240 | 241 | ### Unselecting 242 | A click anywhere deselects anything. This works if you're just clicking around the page and want to deselect something, or if you click to assign an army somewhere. The assigning workflow still occurs, but the final click deselects everything so that no residual highlights are left over. 243 | 244 | ``` 245 | search @session @event @browser 246 | [#click] 247 | highlighted = [#highlighted] 248 | 249 | commit @session @browser 250 | highlighted -= #highlighted 251 | ``` 252 | 253 | ### Unassigning 254 | If there's an army in a slot that's clicked, this block removes that army from that slot. 255 | 256 | ``` 257 | search @session @event @browser 258 | [#click element: [#div class:"battle-slot" room side]] 259 | in-slot = [#army room side] 260 | 261 | commit 262 | in-slot.room := none 263 | in-slot.side := none 264 | ``` 265 | 266 | ### Highlight Styling 267 | When either an army or a battle slot is #`highlighted`, I want to add a class to it so I can use CSS to add a visual marker to it. Because I add the #`highlighted` tag to armies and battle slots differently, I need two different blocks to handle those classes. In the case of a highlighted army, the army is highlighted but its corresponding army tab gets the new class apended. 268 | 269 | ``` 270 | search @session @browser 271 | army-tab = [#div class:"army-tab" name] 272 | army = [#army #highlighted name] 273 | 274 | bind @browser 275 | army-tab.class += "highlighted" 276 | ``` 277 | 278 | In the case of a highlighted battle slot, the #`highlighted` tag is already on the #`div` that needs the new class, which gets apended with this block. 279 | 280 | ``` 281 | search @session @browser 282 | battle-slot = [#highlighted class:"battle-slot" room] 283 | 284 | bind @browser 285 | battle-slot.class += "highlighted" 286 | ``` 287 | 288 | ## Army Data 289 | Each army is listed here with its name, its three colors, and a uniform color. 290 | 291 | ``` 292 | commit 293 | [#army name:"Manticore" color1:"gray" color2:"yellow" color3:"green" uniform:"yellow"] 294 | [#army name:"Asp" color1:"lightgreen" color2:"blue" color3:"green" uniform:"green"] 295 | [#army name:"Scorpion" color1:"purple" color2:"orange" color3:"red" uniform:"orange"] 296 | [#army name:"Flame" color1:"red" color2:"yellow" color3:"red" uniform:"red"] 297 | [#army name:"Tide" color1:"blue" color2:"lightblue" color3:"blue" uniform:"lightblue"] 298 | [#army name:"Salamander" color1:"green" color2:"lightgreen" color3:"brown" uniform:"lightgreen"] 299 | [#army name:"Rat" color1:"black" color2:"brown" color3:"black" uniform:"brown"] 300 | [#army name:"Hound" color1:"blue" color2:"brown" color3:"purple" uniform:"brown"] 301 | [#army name:"Condor" color1:"black" color2:"white" color3:"black" uniform:"gray"] 302 | [#army name:"Squirrel" color1:"green" color2:"gray" color3:"blue" uniform:"gray"] 303 | [#army name:"Rabbit" color1:"white" color2:"gray" color3:"red" uniform:"red"] 304 | [#army name:"Leopard" color1:"orange" color2:"brown" color3:"orange" uniform:"brown"] 305 | [#army name:"Centipede" color1:"orange" color2:"blue" color3:"red" uniform:"blue"] 306 | [#army name:"Phoenix" color1:"yellow" color2:"orange" color3:"red" uniform:"yellow"] 307 | [#army name:"Dragon" color1:"gray" color2:"orange" color3:"gray" uniform:"orange"] 308 | [#army name:"Ferret" color1:"white" color2:"lightblue" color3:"black" uniform:"lightblue"] 309 | [#army name:"Badger" color1:"red" color2:"white" color3:"black" uniform:"red"] 310 | [#army name:"Griffin" color1:"yellow" color2:"brown" color3:"purple" uniform:"purple"] 311 | [#army name:"Tiger" color1:"orange" color2:"black" color3:"white" uniform:"orange"] 312 | [#army name:"Spider" color1:"green" color2:"black" color3:"purple" uniform:"purple"] 313 | ``` 314 | 315 | ## Styles 316 | There's a lot of CSS this time around because of a greater need for media queries on this example, so it's been split up into the style sheets for each page 317 | 318 | ### Broadcast Page 319 | ```css 320 | .broadcast { 321 | height: 100%; 322 | background: #333; 323 | display: flex; 324 | flex-direction: column; 325 | overflow: scroll; 326 | user-select: none; 327 | } 328 | 329 | .battle-room { 330 | color: white; 331 | font-family: "Play", sans-serif; 332 | text-transform: uppercase; 333 | text-align: center; 334 | display: flex; 335 | flex-direction: row; 336 | align-items: center; 337 | justify-content: center; 338 | padding: 5px 0px; 339 | position: relative; 340 | } 341 | 342 | .room-title { 343 | color: #316282; 344 | position: absolute; 345 | } 346 | 347 | .matchup { 348 | display: flex; 349 | flex-direction: row; 350 | flex: 0 0 auto; 351 | } 352 | 353 | .army-card { 354 | display: flex; 355 | flex-direction: row; 356 | align-items: center; 357 | font-family: "Play", sans-serif; 358 | transform: skew(-20deg); 359 | background: #777; 360 | } 361 | 362 | .empty-room { 363 | color: #555; 364 | } 365 | 366 | .teamB > .army-name { 367 | order: 2; 368 | } 369 | 370 | .teamB > .ribbon { 371 | order: 1; 372 | } 373 | 374 | .teamB > .ribbon div { 375 | border-left: 0px solid #222; 376 | border-right: 1px solid #222; 377 | } 378 | 379 | .army-name { 380 | flex: 1 0 auto; 381 | text-transform: uppercase; 382 | transform: skew(20deg); 383 | } 384 | 385 | .ribbon { 386 | box-sizing: border-box; 387 | display: flex; 388 | flex-direction: row; 389 | } 390 | 391 | .ribbon div { 392 | flex: 1 0 auto; 393 | border-left: 1px solid #222; 394 | } 395 | 396 | .red { 397 | background: #EE0034; 398 | color: white; 399 | } 400 | 401 | .orange { 402 | background: #FF7900; 403 | color: black; 404 | } 405 | 406 | .yellow { 407 | background: #FCCC0A; 408 | color: black; 409 | } 410 | 411 | .green { 412 | background: #00933C; 413 | color: white; 414 | } 415 | 416 | .lightgreen { 417 | background: #6CBE45; 418 | color: black; 419 | } 420 | 421 | .blue { 422 | background: #0039A6; 423 | color: white; 424 | } 425 | 426 | .lightblue { 427 | background: #00A1DE; 428 | color: white; 429 | } 430 | 431 | .purple { 432 | background: #A626AA; 433 | color: white; 434 | } 435 | 436 | .black { 437 | background: #000000; 438 | color: white; 439 | } 440 | 441 | .white { 442 | background: #ffffff; 443 | color: black; 444 | } 445 | 446 | .gray { 447 | background: #A7A9AC; 448 | color: white; 449 | } 450 | 451 | .brown { 452 | background: #996633; 453 | color: white; 454 | } 455 | 456 | @media (max-width: 1279px) { 457 | 458 | .broadcast { 459 | width: 100%; 460 | max-width: 250px; 461 | min-width: 220px; 462 | overflow: scroll; 463 | } 464 | 465 | .battle-room { 466 | border-bottom: 1px solid #999; 467 | flex: 0 0 67px; 468 | } 469 | 470 | .room-title { 471 | font-size: 20px; 472 | line-height: 20px; 473 | left: 8px; 474 | } 475 | 476 | .matchup { 477 | flex-direction: column; 478 | } 479 | 480 | .army-card { 481 | height: 20px; 482 | width: 130px; 483 | } 484 | 485 | .versus-text { 486 | font-size: 10px; 487 | line-height: 16px; 488 | } 489 | 490 | .empty-room { 491 | font-size: 14px; 492 | line-height: 14px; 493 | } 494 | 495 | .army-name { 496 | font-size: 12px; 497 | } 498 | 499 | .ribbon { 500 | height: 20px; 501 | width: 30px; 502 | } 503 | 504 | } 505 | 506 | @media (min-width: 1280px) and (max-width: 1499px) { 507 | 508 | .broadcast { 509 | width: 100%; 510 | max-width: 550px; 511 | overflow: scroll; 512 | } 513 | 514 | .battle-room { 515 | flex: 1 0 55px; 516 | } 517 | 518 | .room-title { 519 | font-size: 28px; 520 | line-height: 24px; 521 | left: 8px; 522 | } 523 | 524 | .army-card { 525 | height: 30px; 526 | width: 200px; 527 | } 528 | 529 | .versus-text { 530 | font-size: 16px; 531 | line-height: 30px; 532 | } 533 | 534 | .empty-room { 535 | font-size: 14px; 536 | line-height: 14px; 537 | } 538 | 539 | .army-name { 540 | font-size: 18px; 541 | } 542 | 543 | .ribbon { 544 | height: 30px; 545 | width: 50px; 546 | } 547 | 548 | .teamA { 549 | margin-right: 10px; 550 | } 551 | 552 | .teamB { 553 | margin-left: 10px; 554 | } 555 | 556 | } 557 | 558 | @media (min-width: 1500px) and (max-width: 1679px) { 559 | 560 | .broadcast { 561 | width: 100%; 562 | max-width: 750px; 563 | } 564 | 565 | .battle-room { 566 | flex: 1 0 100px; 567 | } 568 | 569 | .room-title { 570 | font-size: 48px; 571 | line-height: 48px; 572 | left: 10px; 573 | } 574 | 575 | .army-card { 576 | height: 40px; 577 | width: 280px; 578 | } 579 | 580 | .versus-text { 581 | font-size: 20px; 582 | line-height: 40px; 583 | } 584 | 585 | .empty-room { 586 | font-size: 24px; 587 | } 588 | 589 | .army-name { 590 | font-size: 22px; 591 | } 592 | 593 | .ribbon { 594 | height: 40px; 595 | width: 90px; 596 | } 597 | 598 | .teamA { 599 | margin-right: 12px; 600 | } 601 | 602 | .teamB { 603 | margin-left: 12px; 604 | } 605 | 606 | } 607 | 608 | @media (min-width: 1680px) { 609 | 610 | .broadcast { 611 | width: 100%; 612 | max-width: 960px; 613 | } 614 | 615 | .battle-room { 616 | flex: 1 0 120px; 617 | } 618 | 619 | .room-title { 620 | font-size: 60px; 621 | line-height: 40px; 622 | color: #316282; 623 | position: absolute; 624 | left: 20px; 625 | } 626 | 627 | .army-card { 628 | height: 40px; 629 | width: 320px; 630 | display: flex; 631 | flex-direction: row; 632 | align-items: center; 633 | font-family: "Play", sans-serif; 634 | transform: skew(-20deg); 635 | background: #777; 636 | } 637 | 638 | .versus-text { 639 | font-size: 30px; 640 | line-height: 40px; 641 | } 642 | 643 | .empty-room { 644 | font-size: 30px; 645 | line-height: 40px; 646 | color: #555; 647 | } 648 | 649 | .teamA { 650 | margin-right: 20px; 651 | } 652 | 653 | .teamB { 654 | margin-left: 20px; 655 | } 656 | 657 | 658 | .army-name { 659 | font-size: 24px; 660 | } 661 | 662 | .ribbon { 663 | height: 40px; 664 | width: 100px; 665 | } 666 | 667 | } 668 | ``` 669 | 670 | ### Control Page 671 | ```css 672 | .control { 673 | display: flex; 674 | flex-direction: column; 675 | align-items: center; 676 | background: #333; 677 | width: 450px; 678 | overflow: scroll; 679 | position: relative; 680 | user-select: none; 681 | } 682 | 683 | .logo { 684 | width: 100px; 685 | margin: 25px 0px; 686 | } 687 | 688 | .control-lists { 689 | display: flex; 690 | width: 400; 691 | } 692 | 693 | .title { 694 | font-size: 20px; 695 | line-height: 20px; 696 | text-transform: uppercase; 697 | text-align: center; 698 | font-family: "Play", sans-serif; 699 | order: -10; 700 | margin-bottom: 5px; 701 | } 702 | 703 | .all-rooms { 704 | color: white; 705 | flex: 1 0 200px; 706 | display: flex; 707 | flex-direction: column; 708 | align-items: center; 709 | } 710 | 711 | .room { 712 | width: 200px; 713 | margin-bottom: 30px; 714 | flex: 0 0 auto; 715 | display: flex; 716 | flex-direction: column; 717 | align-items: center; 718 | } 719 | 720 | .battle-slot { 721 | height: 26px; 722 | width: 162px; 723 | border-radius: 4px; 724 | border: 1px solid white; 725 | cursor: pointer; 726 | } 727 | 728 | .vs-line { 729 | line-height: 16px; 730 | font-size: 16px; 731 | } 732 | 733 | .all-armies { 734 | color: white; 735 | flex: 1 0 200px; 736 | display: flex; 737 | flex-direction: column; 738 | align-items: center; 739 | } 740 | 741 | .army-tab { 742 | width: 160px; 743 | height: 24px; 744 | font-size: 20px; 745 | line-height: 24px; 746 | text-align: center; 747 | text-transform: uppercase; 748 | border-radius: 4px; 749 | margin-bottom: 10px; 750 | font-family: "Play", sans-serif; 751 | cursor: pointer; 752 | } 753 | 754 | .highlighted { 755 | box-shadow: 0px 0px 10px 5px #bbb; 756 | } 757 | 758 | .battle-slot.highlighted { 759 | background: #bbc; 760 | } 761 | 762 | @media (max-width: 1279px) { 763 | 764 | .battle-slot { 765 | height: 22px; 766 | width: 82px; 767 | } 768 | 769 | .logo { 770 | width: 80px; 771 | margin: 18px 0px; 772 | } 773 | 774 | .title { 775 | font-size: 12px; 776 | line-height: 14px; 777 | width: 70px; 778 | margin-bottom: 2px; 779 | } 780 | 781 | .control { 782 | width: 200px; 783 | } 784 | 785 | .control-lists { 786 | width: 200; 787 | } 788 | 789 | .all-rooms { 790 | flex: 1 0 80px; 791 | } 792 | 793 | .room { 794 | width: 80px; 795 | margin-bottom: 20px; 796 | } 797 | 798 | .vs-line { 799 | line-height: 12px; 800 | font-size: 10px; 801 | } 802 | 803 | .all-armies { 804 | flex: 1 0 80px; 805 | margin-top: 14px; 806 | } 807 | 808 | .army-tab { 809 | width: 80px; 810 | height: 20px; 811 | font-size: 12px; 812 | line-height: 20px; 813 | } 814 | 815 | } 816 | ``` 817 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Eve 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eve Examples 2 | 3 | These examples will showcase the functionality of Eve and demonstrate some of the basic methods that could be used to build various practical applications. The intention is to build an intuition for how Eve works, and in the future combine the different smaller examples here into a larger example that more closely resembles the sort of app that a user might want to build to run website or business on. -------------------------------------------------------------------------------- /assets/fr-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witheve/eve-examples/7dfe7e61da39fa8d1e509bc439efbd8051725074/assets/fr-bold.ttf -------------------------------------------------------------------------------- /assets/fr-fal1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witheve/eve-examples/7dfe7e61da39fa8d1e509bc439efbd8051725074/assets/fr-fal1.ttf -------------------------------------------------------------------------------- /assets/fr-fal2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witheve/eve-examples/7dfe7e61da39fa8d1e509bc439efbd8051725074/assets/fr-fal2.ttf -------------------------------------------------------------------------------- /assets/fr-title.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witheve/eve-examples/7dfe7e61da39fa8d1e509bc439efbd8051725074/assets/fr-title.ttf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------