Couple sell everything they own and risk bankruptcy to live on an old school bus
Tawny MacVay, 34, and her husband Michael, 31, had a family gym business, a three-storey house, and a nice car. But they weren’t happy.
They decided to make a change, selling everything they owned so they could buy a bus and travel the world.
But overhauling your life isn’t so simple.
While the couple found buyers for their home and business, both fell through after they had bought their bus, meaning they were near bankruptcy and had to live with family for six month.
They didn’t let that roadblock stop them from pursuing their dreams of a new lifestyle. In December 2018 they managed to sort out their finances and get to work on turning an old school bus into a home.
The bus, which Tawny and Michael call Oliver, was originally a 2004 school bus they bought for $5,000 (£3,900). Renovations – including creating a kitchen and adding a cosy bed, set them back $15,000 (£11,700).
In May 2018, they moved aboard full-time. They haven’t looked back since.
Advertisement Advertisement
‘We owned a business, a three-storey home and all the usual means of measuring success,’ said Tawny. ‘We were classed as successful and should have been wildly happy, but we weren’t.
‘In early spring of 2018 we saw a documentary about a couple who converted a school bus into a tiny home. We did some research and put our house on the market a few weeks later.
‘Within two months, we had sold most of our belongings, and had the house and our business under contract. We searched and searched for the perfect bus, finally found it, and with everything tied up, we bought the bus and jumped in.’
The couple’s bus now has a living area with a Murphy bed, a full kitchen with jobs and a fridge, a bathroom with a composting toilet, a shower, a washer/dryer, and a bedroom.
It’s powered not just by petrol but with solar panels stuck on top.
So far Tawny, Michael, and Oliver, have journeyed more than 5,000 miles, travelling through the Pacific Northwest, Idaho, Montana, and Wyoming, and plan to head south to avoid the cold through winter.
The pair sleep and relax on the bus, jumping off to take their Great Dane, Apollo, for walks and stopping in cities to fill up on water. They now both work as writers, too.
As we said, it’s not an easy ride, but the couple are so glad they packed up things and joined the bus life.
‘We’ve been very fortunate to not have too many problems while on the road,’ says Tawny.
Advertisement Advertisement
‘When we told our family and friends what we were going to do they laughed. Mike and I are pretty impulsive people and also don’t stay inside the societal norm box. They nodded and went along with it I suppose they are used to our ‘oddball’ choices.
‘We have two kids who also travel with us part-time when they’re not with their biological dad. We like that we can teach them to follow their dreams even if they aren’t ‘normal’.
‘Every lifestyle comes with pros and cons. Having a home on wheels isn’t always an easy choice, it takes determination as you have breakdowns and travel issues to deal with.
 If you understand that, if you feel something when we say that, bus life could be for you.
‘People who say ‘I wish I could do that’ guess what? You can! We don’t have special skills, we had no experience before we got started, we learnt as we went on and with help from others with experience.
Advertisement
‘All it takes is the determination and the desire to push through no matter what.’


{% with company=role.companies.single.0 %}
{% include 'cvgraph/_node_detail_link.html' with url='cvgraph:role_detail' node=role text=role.name %},
{% include 'cvgraph/_node_detail_link.html' with url='cvgraph:company_detail' node=company text=company.name %}
{% endwith %}
{% if user.is_authenticated %}
{% cvgraph_node_delete "button" role %}
{% cvgraph_node_delete "editor" role %}
{% endif %}
{% for topic in role|cvgraph_deep_topics %}
{% include 'cvgraph/_node_detail_link.html' with url='cvgraph:topic_detail' node=topic text=topic.name %}
{% endfor %}
{% with projects=role.projects %}
{% if projects %}
{% for project in projects %}
{% include 'cvgraph/_node_detail_link.html' with url='cvgraph:project_detail' node=project text=project.name %}


Skills
- Languages: Swift, Objective-C, Ruby, Python, Java, Groovy, JavaScript, TypeScript, HTML, CSS, C#
- Apple Technologies: Location services, iOS background processing, Swift Package Manager, localization, Unit and UI tests using XCTest and Quick/Nimble, Core Data, Realm, HTTP requests, MVC, Coordinator architecture, Objective-C and Swift command line applications, launchd
- Development Tools: Xcode, JetBrains IDEs, Bazel, Fastlane, Jenkins, Anka, Crashlytics, Fabric, Firebase, Travis CI, Git, GitHub, Bitbucket, shell scripting
- Other Technologies: Node.js/Express, PostgreSQL, MSSQL, MongoDB, Sass, Gulp, usage and design of Rest APIs, Markdown, AWS (S3, Route 53, API Gateway, Lambda), hosted virtual private servers, Heroku, Bash/ZSH/Fish, Jamf Pro/Casper Suite, AirWatch, Jekyll
Open Source Projects | github.com/hisaac
Plain Pasta
GitHub | Mac App Store
Plain Pasta is a macOS app that automatically strips any styling from text on your Mac's clipboard
-
Developed a more accurate text-filtering engine than any other app with similar functionality, looking for specific
NSPasteboard
types, rather than completely clobbering any text on the pasteboard. -
Created a custom Combine publisher for monitoring changes to a given
NSPasteboard
, made available as an open source Swift Package: github.com/hisaac/PasteboardPublisher. - Released to the Mac App Store
- Currently using SwiftUI in development builds
Tiime
GitHub | iOS App Store
Time is a clock that displays time in non-standard ways, and allows for user customization of the app’s icon, colors, and fonts.
- No third party dependencies
- Used the Coordinator pattern for the app’s architecture
-
Added theme customization using
UIAppearance
and otherUIKit
methods - Implemented timers and date math to calculate the different clocks
- Beta tested through TestFlight, and released to the iOS App Store
Work Experience
Target
Senior iOS Platform Engineer (April 2021 – present)
While at Jamf, I developed an interest in development tooling and automation. Joining Target's iOS Platform team gave me an opportunity to learn more about "platform" development — the intersection of app development and devops. Target already had a mature and well-developed process, so joining their team meant I could learn existing best-practices and build from that foundation.
- Quickly learned how to use and modify Target's multiple in-house and third party build and development tools.
- During my first month at Target, I delivered an important update to Target's complex and custom Bazel integration to support Xcode 12.5, keeping the developers on the most recent development tools available.
Jamf Software
iOS and macOS Developer (March 2019 – April 2021)
I joined Jamf because I wanted to develop iOS and macOS software that would impact people, and it did not disappoint. Jamf’s software is running on millions of devices across the globe. I’ve worn a few hats at Jamf, first working on a user-facing iOS and macOS app, then moving to work on a low level macOS application with a command line interface — the backbone of Jamf’s Mac management platform.
- Greatly improved the VoiceOver compatibility of Self Service, Jamf’s primary user-facing macOS app
- Helped maintain and improve my team’s multiple testing suites and CI/CD workflows, using Jenkins and Anka infrastructure
- Took ownership of an internal server-side Swift project
- Investigated, advocated for, and implemented multiple large and small features and improvements to the codebase
- Worked across countries and time zones with coworkers on my team, and outside my team
Q3 Contracting
iOS Developer (July 2018 – March 2019)
As Q3’s sole iOS developer, I maintained multiple existing iOS apps and libraries written in both Swift and Objective-C, wrote new apps based on the business’s needs, and help decide the direction of mobile development for the company.
- Created a brand new iOS app that displays contact information for all employees in the company
-
Updated multiple apps to more modern development practices, and retired apps no longer being used
- Migrated analytics, crash reporting, and beta deployment to Firebase (from Mixpanel, Fabric/Crashlytics, and DeployGate respectively)
- Added Carthage support to the company's iOS libraries
-
Removed unnecessary external dependencies, and replaced them using native
UIKit
/Foundation
APIs where possible - Improved the build and deploy process for apps that rely on multiple environments (dev, test, prod, etc.)
- Volunteered to manage the company’s Jenkins server, and improved the build processes for both web and mobile applications
- Worked with web developers to build a new Node.js microservice in TypeScript
- Assisted in technical interviews and onboarding for new developers joining the team
C.H. Robinson
iOS Developer (July 2017 – July 2018)
After a three month stint as a web developer, I moved onto the mobile team and took over all development of the company’s two iOS apps. Having not done any iOS development prior to this, I needed to learn quickly to keep the apps functioning, and deliver new features.
- Learned Swift and iOS development quickly, and was pushing code to production in less than a month
- Tested and implemented a complex system for monitoring and reporting a device’s location while the app was in the background
- Internationalized one of our apps for use in Europe, adding 7 new language translations, updating the UI to handle variable-length strings, and making significant updates to the app’s networking logic to address latency issues
Full-Stack Web Developer (April 2017 – July 2017)
I was hired for C.H. Robinson’s HatCHRy program — a three month, intensive on-boarding program for new developers — where I learned all about the company’s business and technology. It was here that I learned C#/.NET, and used it to develop an internal back end service that sent email notifications upon successful upload of accounting documents.
Pixelsmith
Contract Front End Web Developer (February 2017 – April 2017)
Pixelsmith is a small development shop specializing in web app and Wordpress app design. While here, I helped build and design a banking rewards web app.
BBDO Minneapolis
IT Support Technician (August 2014 – April 2016)
I managed the IT department and handled all the tech support for BBDO’s Minneapolis office, winning an award for my efforts helping the company. Upon starting at BBDO, I streamlined processes and systems to make life easier for everyone.
- Support: macOS, Windows, iOS, A/V gear
- Manage: projects, inventory, servers, Active Directory
- Execute: Sarbanes-Oxley compliance testing, documentation, budget planning
- Technologies Used: macOS, macOS Server, Windows 7/10, Windows Server, iOS, Active Directory, VMWare, Parallels, FileMaker Pro, Shell scripting, Jamf's Casper Device Management
Apple Inc.
Genius (August 2012 – August 2014)
Apple Store Southdale - Edina, MN
My Genius team won Apple’s Ovation award two quarters in a row, demonstrating our excellence in customer satisfaction and repair efficiency. As a Genius, I performed Mac and iOS hardware and software support, troubleshooting, and training.
Quality Assurance Engineer (August 2013 – November 2013)
Apple Corporate - Cupertino, CA
Only a few Apple employees are chosen for this special program. I got to spend three months at Apple's headquarters in Cupertino, learning what it takes to secure web applications. I worked directly with the web development team, giving UX and workflow feedback, testing apps using web automated tools, and testing security using tools like OWASP ZAP.
Education
Prime Digital Academy
Full Stack Software Engineering Certificate (September 2016 – January 2017)
Prime is an intensive, 18-week web development school, focused on both technical skills and business skills. Here, I learned HTML, CSS, and full-stack JavaScript development, and also honed my public speaking, project management, and group work skills.
Institute of Production and Recording
AAS Degree in Audio Production and Engineering (2008–2010)
- Graduated valedictorian
- On the Dean’s list throughout my attendance
- Elected to student advisory board
University of North Dakota
Fulfilled General Education Requirements (2005–2008)
- Accepted into the Honors program
- Elected Honors Program Student Senator
References
For the privacy of my references, I don't include their contact information here. Please email me at<EMAIL_ADDRESS>to request references.


Please do not try to access intermails not meant for you. Thank you.
Oskar Jönsson
Managing Consultant
Head of IT security


use of as the delimiter instead of in:
Use v-for to iterate an object:
Provide a second argument for the key:
And another for the index:
Displaying filtered/Sorted results
v-for with a Range
v-for on a <template>
v-for with v-if
No todos left!


|
Example of Reflow Soldering Condition
|
|
|
Double heating method with infrared heater.
|
|
|
2. Temperature measurement
|
|
Thermocouple 0.1 to 0.2 Φ CA (K) or CC (T) at soldering portion (copper foil surface). A heat resisting tape should be used for fixed measurement.
|
|
|
|
|
|
A(℃) 3s max. | B(℃) | C(s) | D(℃) | E(℃) | F(s) | | 260 | 230 | 40 | 180 | 150 | 120 |
|
|
|
(1) The condition mentioned above is the temperature on the mounting surface of a PC board. There are cases where the PC board's temperature greatly differs from that of the switch, depending on the PC board's material, size, thickness, etc. The above-stated conditions shall also apply to switch surface temperatures.
|
|
|
(2) Soldering conditions differ depending on reflow soldering machines. Prior verification of soldering condition is highly recommended.
|
|
Reference for Hand Soldering
|
| Soldering temperature | 330±5℃ | | Soldering time | 3s max. |
|
|


ECIP - Minciu Oana-Anastasia
[[${#httpServletRequest.remoteUser}]]
Give Course Feedback
See Feedback Results


How to make your own Operating System
Don’t build your own Operating System
I didn't really build an entire operating system
- WE built an operating system
- multi-discipline team
- Hardware
- Software
- Business
Ninja Sphere
- Variscite SOM
- AM35xx SOC
- WiLink8
- 256/512MB RAM & 128MB flash
Custom Base board
- Zigbee Mesh
- GestIC
- 16 x 16 RGB led matrix
- ATmega328
OS Components
- Bootloader
- Kernel
- Userland
u-boot
Linux Kernel
Userland
- Yocto Project 2014..
- Ubuntu 14.04
Recovery
Yocto
- Small footprint, ~60MB
- Solid Distribution
- Like "gentoo" stage1 install
- Not very developer friendly
- Security updates are your problem
Ubuntu
- Everyone is familiar with apt
- Massive number of packages
- Security Updates, even for ARM!
- Tons of resources
- Upstart, Systemd (coming soon)
Pipelines
- Automation using buildkite
- 1000s of deb packages
DRY Packaging
- built a wrapper for FPM
- convention over configuration
- bash
- s3cmd
- aptly
Developers
Lessons Learnt
- apt-get update.....
- snappy?


RETSU
Focus Point: Jade Gate Chakra
Concept: RETSU Is Where We Transmute The Limits Of Perception By Remembering Our Wholeness As Spirit
Mantra Meaning: Everything Flows/Is Elevated To The Divine
Possible Effects: Enhances Your Perception And Mastery Of Space-Time Dimensions
Mantra
- JYOTA HI (Light)
- CHANDOGA (Sound)
- JIVA (LIFE)
- TAY SWAHA (Flows/Elevated And Refined Experience)
Gods In Religion For Added Visualization
Shinto:
Nifu Daimyojin, known as the deity of Red Life. They refer to the red energy of life that cannot be extinguished and that keeps us alive longer.
Buddhists:
Amida Nyorai, or Amithabha Buddha, the Light and Long Life, again in reference to the eternity of our existence, or the extending of our lifespan.
How It Works
After Practicing The Kuji-In Exercises For Some Time, They Will Alter Your Perception Of Gross Matter So You Will Be Able To Perceive The Different Flows Of Energy Composing Our Space-Time Multi-Dimensional Universe.

Imagine That The Atoms Of The Universe Are Composed Of Energy Waves Instead Of Rigid, Inflexible Solid Matter
Feel The Flexibility Of The Structure Of These Energy Waves. Understand That These Energy Waves Build Your Body.
YOU ARE BEING CONTINUOUSLY RE-CREATED!
  Don’t Imagine Or Invent Them...SIMPLY PAY ATTENTION
Mudra
The Index Of The Human Hand Thrusts Its Affirmation Of Life To Climb Into The Channel Of The Spirit, Finally Getting In Contact With The Spiritual Expression And Consciousness, The Spiritual Index And Thumb.
The Human Mind (Represented By The Air/Index Finger Of The Left Hand) Is Wrapped In Every Dimension Of The Spirit Communing With The Spiritual Void And Air Element, Un-Worded Consciousness And Absolute Truth
Represents
- The Rising Of The Kundalini (The Power Of Life That Resides In The Base Chakra, And Climbs Up The Spine When Awakened)
- The Elevation Of The Mind To Commune With The Higher Self
- The Human That Crosses All The Spiritual Dimensions To Attain Enlightenment
Meaning
Split (The First Thing That Could Be Perceived As Splitting Up, In Creation, Is The Apparition Of The Concept Of Dimensions)
Retsu Corresponds To The Multi-Dimension Universe We Live In.
This Technique Is A Link Between These Dimensions, And It Can Be Used To Go Up And To Bind Them, Establishing Relationships With Many Dimensions At The Same Time
Concept
We Now Stand In A Transit Area Between Dimensions.
Space And Time Are Only Definitions That We Placed Over The Mask Of Our Own Perception.
From Spirit To Human, And Human To Spirit, There Is No Label, Name, Difference Or Anything Else That Could Be Defined In Ways To Separate In Levels Or Compartments
RETSU Is Where We Transmute The Limits Of Perception.
It Is Where Long Hours Stop Being Tiring, And Too Short Moments Do Not Seem So Fragile Anymore.
Henceforth, Human Time Becomes Of Less Importance.
The Same Thing Goes With Perception Of Space. Too Small Or Too Big Are Concepts That Disappear, Leading To The Appreciation Of Whatever The Size.
From The Moment You Remember Your Wholeness As Spirit, The Eternity And The Size Of Your Existence, Every Measure


Pikmin illustrations by
marcolago
Following Content
Blue Front
Blue Back
Red Front
Red Back
After Blue Front
After Blue Back
Middle of Blue Front
Middle of Blue Back
Following Content
After Blue Front, Again


Repetition is what computers are really good at; you can ask them to do the same thing over and over again and they will never grow bored. This is the main advantage of using code as a design tool; you don’t have to do everything by yourself again and again. Once your code works for a single case, you can sit back and let the computer handle the next dozen, hundred, or bajillion items.
for
LoopIn programming circles, doing the same thing multiple times is generally called "iteration" or, more colloquially, a "loop". Python provides a concise syntax for looping over a
set of values through its "for … in …:
" syntax. The first ellipsis should
be replaced with a variable name – the loop's "counter". The second ellipsis should be a list
or other sequence type (to be stepped through item-by-item).
The statement should look like this:
for variable in sequence: ... # commands
Don’t forget the colon at the end of the for
statement! The colon and the indentation of the next line(s) are how Python recognizes the beginning and end of the loop.
The variable can have any name you like (as you already know if you've read about variables). This variable holds the "current" item as you step through the indented code again and again.
A common name for it is i
, especially in cases where you're looping over a sequence of numbers and the variable is functioning as a "counter". This is an echo from basic math where counters are generally named i
as well.
It’s short, and it’s a good convention with a long history.
The sequence part is a collection of elements, most commonly a list. The elements of the sequence can be anything you like: numbers, strings, colors, and so on. Each element is consecutively assigned to the variable.
So if sequence were [
, the commands in the for-loop would be executed three times. The variable will be assigned "ready"
, "steady"
, "GO!"
]"ready"
the first time through the loop, "steady"
the second time, and "GO!"
the third time:
for item in ["ready", "steady", "GO!"]: print(item) >>> ready >>> steady >>> GO!
If you want your loop to execute a fixed number of times, try using the built-in range() function. When you use a range, the loop will run once for each value from zero up to (but not including) the number you pass as the range argument.
for item in range(3): print(item) >>> 0 >>> 1 >>> 2
The commands part consists of one or more PlotDevice commands that need to be executed repeatedly. They are indented beneath the for-statement. The script is ‘trapped’ in a loop here and keeps executing the indented commands until each element from list has consecutively been assigned to variable.
What makes this powerful is being able to use the variable's value in each loop. You'll notice that in both of the previous examples the commands part is identical. It's simply printing things out.
Calling print
item
is an "abstract" command: when PlotDevice
goes through the loop item is replaced with each element in the list. Thus you get different output each time the line executes (even though it's the same code statement every time).
You can use either of the collection types (lists or dicts) as the sequence in a loop. So far we've seen the "typical" case in which you loop over the elements of a list, but both types provide some handy mechanisms for controlling how you step through them.
Using a simple "for … in" loop only gives you the values from the list. If you also need the index within the loop, use the convenient enumerate() command. It provides you with a sequence of index/value pairs, one for each item in the list:
shortlist = ['first', 'middle', 'last'] for i, name in enumerate(shortlist): print("item", i, "is:", name) >>> item 0 is first >>> item 1 is middle >>> item 2 is last
Dictionaries support the same "for … in" syntax as lists. But where list iteration steps through values, dict iteration steps through keys. Here we iterate through keys and manually retrieve the values from our dictionary:
info = dict(series=9000, instructor="Langley", operational="1992/01/12") for key in info: val = info[key] print(key, val) >>> series 9000 >>> instructor Langley >>> operational 1992/01/12
You can also iterate through key/value pairs using the dict.items() method. In this case your for statement should declare two variables (separated by a comma) to refer to the key and value respectively:
for key, val in list(info.items()): print(key, val)
For-loops and the random() command work really well together. You can draw a hundred circles for example, and have PlotDevice place each of them at random locations. This way, each time you run the script, you get a different composition of circles.
It’s easy to program a ‘style’ with variations this way.
... # set up your style # draw randomly in that style for i in range(100): x = random(HEIGHT) y = random(WIDTH) r = random(25, 50) arc(x, y, r)
You can let the computer decide on its own which items to draw from the list by using the choice() command. Each time you call choice() it returns a randomly selected value from the list. Since each of these "choices" is independent, it's quite possible that you'll receive the same value more than once during a sequnce of calls.
If your goal is to reorder the items in a list while ensuring that each element only appears once, use the shuffled() command. Calling shuffled() doesn't actually modify the list you pass to it; instead it generates a new, permuted list with the same values as your original. You can store this permutation in another value, or iterate through it immediately:
alphabet = ['a','b','c','d'] print(shuffled(alphabet)) >>> ['b', 'c', 'a', 'd'] for letter in shuffled(alphabet): print(letter) >>> a >>> c >>> d >>> b
Here's a dead-simple example of how you would use random choice with small lists to generate compositions. The script defines a pair of lists; one of possible words, and another with selected colors. Everything beyond point in the script is entirely stochastic and will generate different output on every run.
It picks random words and colors from the two lists then draws them to the screen with random sizes and rotations. It retains a certain coherence between different runs because of the limited set of words and colors.
words = ["Lists", "are", "fun"] colors = [color(1,0,0), color(1,1,1), color(0,0,0)] for i in range(40): x = random(WIDTH) y = random(HEIGHT) rotate(random(360)) fontsize(random(10,100)) fill(choice(colors)) text(choice(words), x, y)


Next: multiple-value-prog1, Previous: multiple-value-call, Up: Data and Control Flow Dictionary
multiple-value-list
form ⇒ list
form—a form; evaluated as described below.
list—a list of the values returned by form.
multiple-value-list evaluates form and creates a list of the multiple values_2 it returns.
(multiple-value-list (floor -3 4)) ⇒ (-1 1)
values-list, multiple-value-call
multiple-value-list and values-list are inverses of each other.
(multiple-value-list form) ≡ (multiple-value-call #'list form)


WEB DESIGNER
Yatish Somname was born on 1992 in Kathmandu, Nepal
Lives and works in Bay Area, CA
EDUCATION
2009
Santa Rosa High School
2015
Santa Rosa Junior College, Santa Rosa, CA, Natural Science A.A.
2016 - Contining
Cal State East Bay, Hayward, CA, Multimedia Option, B.A.
Skills
HTML5
CSS
JAVASCRIPT
Photoshop, Illustration
GITHUB
Team player


Try out our Premium Member services: Virtual Legal Assistant, Query Alert Service and an ad-free experience. Free for one month and pay only if you like it.
R/CR.MA/3561/2008 ORDER IN THE HIGH COURT OF GUJARAT AT AHMEDABAD CRIMINAL MISC.APPLICATION NO. 3561 of 2008 With CRIMINAL MISC.APPLICATION NO. 3593 of 2012 In CRIMINAL MISC.APPLICATION NO. 3561 of 2008 ================================================================ VASANT DYEING & PRINTING WORKS & 1....Applicant(s) Versus STATE OF GUJARAT & 1....Respondent(s) ================================================================ Appearance: MR ZUBIN F BHARDA, ADVOCATE for the Applicant(s) No. 1 - 2 MR SUNIL L MEHTA, ADVOCATE for the Respondent(s) No. 2 MR VAIBHAV A VYAS, ADVOCATE for the Respondent(s) No. 2 PUBLIC PROSECUTOR for the Respondent(s) No. 1 ================================================================ CORAM: HONOURABLE MR.JUSTICE ANANT S. DAVE Date : 03/05/2017 ORAL ORDER
1. Heard learned advocates for the parties.
2. Shri Zubin Bharda, learned advocate for the applicants would contend that Section 21 (1) of the Air (Prevention and Control of Pollution) Act, 1981 came to be substituted by the Act 47 of 1987 with effect from 1.4.1988, which reads as under:-
"21. Restrictions on use of certain industrial plants.-- [(1) Subject to the provisions of this section, no person shall, without the previous consent of the State Board, establish or operate any industrial plant in an air pollution control area:
Provided that a person operating any industrial plant in any air Page 1 of 3 HC-NIC Page 1 of 3 Created On Wed Aug 16 16:50:29 IST 2017 R/CR.MA/3561/2008 ORDER pollution control area immediately before the commencement of section 9 of the Air (Prevention and Control of Pollution) Amendment Act, 1987 (47 of 1987), for which no consent was necessary prior to such commencement, may continue to do so for a period of three months from such commencement or, if he has made an application for such consent within the said period of three months, till the disposal of such application.]"
3. The complaint refers to inspection of the premises of the petitioner company carried out on 14.6.1988 and the inspection report was also prepared on the same date. The complaint is filed on 03.11.1988, which refers to non- compliance of substituted Section 21 (1). It is further submitted that Rules framed in exercise of powers conferred by Sub-sections (1) and (2) of Section 54 of the Air (Prevention and Control of Pollution) Act, 1981, in Chapter IV under the heading of 'Prevention and Control of Air Pollution' prescribed under Rule 9 for preferring an application for consent under Sub-section (2) of Section 21. Proviso to above Rule provided that where any person, immediately before the declaration of any area as air pollution control area, operates in such area any industrial plant for the purpose of any industry specified in the Schedule I shall make the application for consent within a period of four months from the date of such declaration of the area.
4. Form-I is prescribed under Rule 9 (1) for preferring application for consent under Section 21 of the Act, 1981 and, therefore, it is submitted that the complaint lacks statutory force and powers invoked by the complainant is contrary to law inasmuch Rule 9 ought to have been amended for bringing period for preferring an application within a period of three months on par with proviso to Sub-section (1) and (2) of Section 21 of the Act.
Page 2 of 3
HC-NIC Page 2 of 3 Created On Wed Aug 16 16:50:29 IST 2017
R/CR.MA/3561/2008 ORDER
5. As against above, Shri Vaibhav Vyas, learned advocate for respondent no.2 would contend that Act 1981 is in force in the State of Gujarat with effect from 30.3.1981 and Gujarat Pollution Control Board came into existence on 15.10.1974. Rules of 1983 were published vide Notification dated 6.8.1984 and all the Districts of the State of Gujarat have been brought within the purview of Air Pollution Control of the Department. According to Shri Vyas, the petitioners have not preferred any application seeking consent either prior to or after 1.4.1988 and further date of complaint is 3.11.1988, admittedly after completion of three months under Sub-section (1) of Section 21 of the Act. Shri Vyas, however, contends that time limit of four months prescribed under Rule on the contrary will benefit the petitioner, giving one month more than what is prescribed under the Act.
6. Adjourned to 5th May 2017.
(ANANT S.DAVE, J.) *malek Page 3 of 3 HC-NIC Page 3 of 3 Created On Wed Aug 16 16:50:29 IST 2017


Recruiting Jobs Latest post about 4 hours ago
- New TaxJar Customer Support Specialist (FT & Weekend)
- New ScheduleOnce Customer Success Manager -USA
- New Vortala Pty Ltd Website Account Manager
- Rise Vision Customer Support Representative
- Mystery Science Customer Experience Specialist (PST Hours)
- Nookal Customer Technical Support - UTC/GMT Only
- Help Scout Customer Champion - PDT through EDT
- ModSquad Inc Community Tier2 Support Associate
- View all 21 Recruiting jobs →


We want to enable the citizens, to look at the STATE OF EXCEPTION, which is continuously established to facilitate the self-aggrandizement and assertiveness of the sovereign. It encroaches upon each of us.
orThe city of Hamburg will secure the political elite in an architecture of power. On the occasion of the G20 presidency summit more than 18.000 police forces will come in July 2017 to Hamburg from all over Europe. Police is gearing up. They bought new tanks, machine guns and high-tech monitoring tools. The German army is surveilling the harbor and the air over Hamburg. New prisons were built up for protesters. Judges will be working for 24 hours in a provisional court houses. Demonstrations in the inner city are forbidden during the summit.
Paul Lovis Wagner visually investigates the state of exception as governance strategy. He documents the preparations and a range of ministers summits all over Germany. In times of growing detachment between the rulers and the ruled, his focus lies on the representations of (in-)security and power.
Dancing takes place with those who are in the ballroom. That equally counts for international politics.
One honestly has to say that the Hamburgers have never been that safe.
The only option to face a disregard of the security zones with restraining order is, due to the particular role and thus arising dangers, the use of immediate enforcement.
There will also be the throne, that the king will bring along. (…) I love something like this. The visit of the king of Saudi-Arabia will get a place of honor in our archives. My pleasure is tremendous. (…) As Hotelier I am foremost neutral. Obviously I would not host people, that are classified as dangerous by the inner secret service.
Heads of autocratic and populist governments have, through the summit, the possibility to see how a vivid democratic society functions and furthermore how intense the involvement can be.
Hereby is imposed, that the constitution will be restricted between the 7 of July from 6 a.m. to the 8 of July 5 p.m. within the subsequent shown space. The constitution will be effected as within the mentioned space all outdoor gatherings are forbidden.
We react proportionally. But a legal prohibition stays a legal prohibition. There is nothing you can do about it.
We have put ourselves in the cells and there is really enough space.
If you believe in the world you precipitate events, however inconspicuous, that elude control (…). Our ability to resist control, or our submission to it, has to be assessed at the level of our every move.


Note that due to version-related issues the code in this post is displayed but is not excecuted/run.
According to the MPlus website, the R package MPlusAutomation serves three purposes:
- Creating related groups of models
- Running batches
- Extracting and tabulating model parameters and test statistics.
Because modeling involves comparing related models, (partially) automating these is compelling. It can make it easier to use model results in subsequent analyses and can cut down on copy and pasting output or results between programs.
So I tried it and liked it; it’s well-designed. Here’s a little example exploring a set of models for which one aspect of its specification changes between models. This example uses built-in data, so you should be able to do everything here in this example, as long as you have purchased and installed MPlus.
First, let’s load the package, which we can install from CRAN using install.packages("MPlusAutomation")
:
# install.packages("MPlusAutomation")
library(MplusAutomation)
I’m going to do something in what is maybe a bit of a clunky way: Hide the directory I’m saving all of the input and output files in by evaluating a line of code but not displaying it here, because it’s not necessary to see and is a pain to copy and paste. The key thing to know is that the variable the_dir
represents the working directory I’m saving these in; you should replace it with the directory - whether it’s a Dropbox folder, on your Desktop, or anywhere else - that you’re saving these in.
We will use the iris
dataset built-in to R. It can be loaded just by typing iris
. Here is how it looks:
head(iris)
Next, we’ll use the super handy prepareMPlusData()
function to prepare a data file in the format MPlus uses, namely, a tab-separated .dat
file with no column names.
prepareMplusData(iris[, -5], paste0(the_dir, "iris.dat"))
Let’s unpack what we’re doing here.
If you recall the contents of
iris
, you’ll note that the fifth column is the name of the species. Our goal in this analysis is to use a general mixture modeling or Latent Profile Analysis (APA) approach in MPlus, and so we’ll only use the continuous variables as predictor variables.The somewhat-inelegant bit of code
paste0(the_dir, "iris.dat")
is basically pasting together are not-so-secretly-hid working directory with a name we chose for this specific.dat
file, namely,iris.dat
. In short, this says save the prepared data file in this particular folder with this particular name. Again, you’ll have to changethe_dir
to be the folder (or working directory) that you chose, set, and are using.
Now, we have to specify the models using MPlus syntax.
We’ll specify three mixture models. The trick is that we’ll save each of the following models (either in a .txt
file or in MPlus style using a .inp.
file type) with different names in the working directory we saved the data file to.
Here we go:
- One for which we estimate different means between two profiles
TITLE: iris LPA
DATA:
File is iris.dat
VARIABLE:
Names are Sepal_Length Sepal_Width Petal_Length Petal_Width;
Classes = c(2) ;
MODEL:
%overall%
Sepal_Length Sepal_Width Petal_Length Petal_Width;
%c#1%
[Sepal_Length Sepal_Width Petal_Length Petal_Width];
%c#2%
[Sepal_Length Sepal_Width Petal_Length Petal_Width];
ANALYSIS:
Type is mixture;
OUTPUT:
Tech11;
- One for which we estimate different means between the two profiles and the model is specified to estimate the correlation (or covariance) for the variables
TITLE: iris LPA
DATA:
File is iris.dat
VARIABLE:
Names are Sepal_Length Sepal_Width Petal_Length Petal_Width;
Classes = c(2) ;
MODEL:
%overall%
Sepal_Length Sepal_Width Petal_Length Petal_Width;
Sepal_Length WITH Sepal_Width Petal_Length Petal_Width;
Sepal_Width WITH Petal_Length Petal_Width;
Petal_Length WITH Petal_Width;
%c#1%
[Sepal_Length Sepal_Width Petal_Length Petal_Width];
%c#2%
[Sepal_Length Sepal_Width Petal_Length Petal_Width];
ANALYSIS:
Type is mixture;
OUTPUT:
Tech11;
- One for which we estimate different means and the model is specified to different covariances (and variable variances) between the two profiles
TITLE: iris LPA
DATA:
File is iris.dat
VARIABLE:
Names are Sepal_Length Sepal_Width Petal_Length Petal_Width;
Classes = c(2) ;
MODEL:
%c#1%
Sepal_Length Sepal_Width Petal_Length Petal_Width;
Sepal_Length WITH Sepal_Width Petal_Length Petal_Width;
Sepal_Width WITH Petal_Length Petal_Width;
Petal_Length WITH Petal_Width;
[Sepal_Length Sepal_Width Petal_Length Petal_Width];
%c#2%
Sepal_Length Sepal_Width Petal_Length Petal_Width;
Sepal_Length WITH Sepal_Width Petal_Length Petal_Width;
Sepal_Width WITH Petal_Length Petal_Width;
Petal_Length WITH Petal_Width;
[Sepal_Length Sepal_Width Petal_Length Petal_Width];
ANALYSIS:
Type is mixture;
OUTPUT:
Tech11;
Again, each of those models has to be saved in the working directory we saved the data in.
Now we can run the models using runModels()
, which runs the model in MPlus. You can see here what I named each of the three files:
# Model 1
runModels(paste0(the_dir, "2-iris-LPA_means.inp"))
# Model 2
runModels(paste0(the_dir, "2-iris-LPA_means_correlated.inp"))
# Model 3
runModels(paste0(the_dir, "2-iris-LPA_means_correlated_free_variances.inp"))
Now we’re in business and can look at the output using readModels()
:
m1 <- readModels(paste0(the_dir, "2-iris-LPA_means.out"))
m2 <- readModels(paste0(the_dir, "2-iris-LPA_means_correlated.out"))
m3 <- readModels(paste0(the_dir, "2-iris-LPA_means_correlated_free_variances.out"))
We can now inspect the fit statistics and other summary information for the three models:
m1$summaries$BIC
m2$summaries$BIC
m3$summaries$BIC
And can examine parameters, as well (ignore the nrow(...)
for now; the last row is not necessary, and this - clunkily, but for now, easily - does not print it):
m1$parameters[[1]][-nrow(m1$parameters[[1]]), ]
m2$parameters[[1]][-nrow(m2$parameters[[1]]), ]
m3$parameters[[1]][-nrow(m3$parameters[[1]]), ]
Cool!
An especially powerful feature of MPlusAutomation
is the ability to use runModels()
and readModels()
in conjunction with the function createModels()
, because these functions allow you to specify an entire folder, in addition to a specific model or output file.
The createModels()
function creates a set of models using a template. Here is an example that would create models with different numbers of profiles, from 1
to 9
. Here is doing that for the model with different means between profiles specified (model, 1 above):
[[init]]
iterators = classes;
classes = 1:9;
filename = "[[classes]]-iris-LPA.inp";
outputDirectory = the_dir;
[[/init]]
TITLE: iris LPA
DATA:
File is iris.dat
VARIABLE:
Names are x1 x2 x3 x4;
Classes = c([[classes]]) ;
MODEL:
%overall%
x1 x2 x3 x4;
[x1-x4];
ANALYSIS:
Type is mixture;
OUTPUT:
Tech11;
We would then run the following series of functions after saving the above-specified model as “iris_lpa_template.inp” (note that we save the output of readModels()
to a list object):
createModels(paste0(the_dir, "iris_lpa_template.inp"))
runModels(the_dir)
models_list <- readModels(the_dir)
We can then inspect specific models using list-indexing: The first model is saved as models_list[[1]]
, for example. Or, we can print the output for all of the models by typing models_list
.
You can learn more about MPLusAutomation here.


Aww Crap.. You're Lost in Space.. Make Sure You're Connected to Internet.Try Reloading The Page. — The Team


Before you start working on Chapter 4, get the 4-begin
codebase. The 4-begin folder is located at the root of the book
directory inside the builderbook repo.
git clone https://github.com/builderbook/builderbook.git
.4-begin
folder, run yarn
to install all packages. These are the packages that we install specifically for Chapter 4:
"aws-sdk"
"email-addresses"
"jest"
"winston"
Check out the package.json for Chapter 4.
Remember to include your .env
at the root of your app. By the end of Chapter 4, you will add Amazon_accessKeyId
, Amazon_secretAccessKey
, Amazon_region
, and EMAIL_SUPPORT_FROM_ADDRESS
environmental variables to your .env
file.
In the previous chapters, we:
In this chapter (Chapter 4), our main goals are:
As our app grows, it takes more time to test our code. When the app's codebase gets large and interconnected, manual testing becomes more time-consuming. To stay productive as a developer, we can create automated tests. You can write a test as small as checking one single object/function (unit test) or as complex as checking multiple interconnected objects (integration test). More on test types.
In this subsection, we get familiar with automated code testing. As an example, we want to test if our code generates the proper slug
in different scenarios. We placed our slug-generating code at server/utils/slugify.js
. You may remember from Chapter 3 that we used the generateSlug()
function to generate a slug
for the User model. We will also use this function to generate a slug for the Book and Chapter models (Chapter 6). However, for testing, we will focus only on the User model.
We will use Jest, a popular JavaScript testing library by Facebook. We will write a simple unit test that checks if the generateSlug()
function generates the proper slug
.
First, we need to decide where to store our unit test. Jest automatically finds files containing test code. We have to either place our files in a __tests__
folder or create a file with a .test.js
extension. We use the second option - for no particular reason.
Below, we will create a slugify.test.js
file to test our code in slugify.js
. We place the former file into test/server/utils/slugify.test.js
. Go ahead and create the new folder and file.
To design our unit test, we should look at the generateSlug(Model, name)
function inside server/utils/slugify.js
and see what this function does:
const _ = require('lodash');
// https://devdocs.io/lodash~4/index#kebabCase
const slugify = (text) => _.kebabCase(text);
async function createUniqueSlug(Model, slug, count) {
const user = await Model.findOne({ slug: `${slug}-${count}` }, 'id');
if (!user) {
return `${slug}-${count}`;
}
return createUniqueSlug(Model, slug, count + 1);
}
async function generateSlug(Model, name, filter = {}) {
const origSlug = slugify(name);
const user = await Model.findOne(Object.assign({ slug: origSlug }, filter), 'id');
if (!user) {
return origSlug;
}
return createUniqueSlug(Model, origSlug, 1);
}
module.exports = generateSlug;
The generateSlug()
function does a few things.
name
into slug
using slugify()
defined at the top of the file.Model.findOne({ slug: origSlug })
slug
does not exist, the function returns the if (!user) {return origSlug;}
slug
and returns this new slug: createUniqueSlug(Model, origSlug, 1
.Ok, we understand what the function does. Now what are the scenarios in which we need to test for proper slug
creation?
Scenario 1. We should test if the function generates the proper slug when no user exists with the same slug
. In this scenario, we basically test if slugify()
converts name
into slug
using _.kebabCase(text)
(see Lodash docs). Let's call this test no duplication
.
Scenario 2. We should test if the function generates the proper slug
when a user with the same slug
already exists. Let's call this test one duplication
.
Scenario 3. Optionally, we should test if the function generates the proper slug
when a user with same slug
already exists and another user with that slug
appended with -1
exists as well. We'll call this test multiple duplications
.
Let's follow the example in Jest's docs on how to write a unit test for a simple function. Here is the function:
function sum(a, b) {
return a + b;
}
export default sum
For this function, the sum checker
test is:
import sum from './sum';
test('sum checker', () => {
expect(sum(1, 2)).toBe(3);
});
Following the example above, for the test no duplication
and function generateSlug()
, you get:
test('no duplication', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John Jonhson').then((slug) => {
expect(slug).toBe('john-jonhson');
});
});
This test says that for a user with name
John Jonhson
, expect the slug
to be john-jonhson
.
The only unknown code in the block above might be expect.assertions(1)
. The number of assertions is the number of expect().toBe()
assertions that you expect inside the test.
In our case, the number of assertions is one. By specifying expect.assertions(1);
- we tell Jest to call that one assertion that we have in the code expect(slug).toBe('john-jonhson-jr-');
. Without specifying expect.assertions(1);
- our assertion may or may not be called. If we specify the wrong number of assertions - our test will fail.
When the number of assertions is 2 or more, use expect.assertions(number)
to make sure that all assertions are called. You can read more about expect.assertions(number)
in the Jest docs.
You just wrote one test - no duplication
. Now write our two other tests (one duplication
and multiple duplications
) and put all three of them into the so called test suite by using Jest's describe(name, fn)
syntax:test/server/utils/slugify.test.js
:
const generateSlug = require('../../../server/utils/slugify');
const MockUser = {
slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john'],
findOne({ slug }) {
if (this.slugs.includes(slug)) {
return Promise.resolve({ id: 'id' });
}
return Promise.resolve(null);
},
};
describe('slugify', () => {
test('no duplication', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John Jonhson.').then((slug) => {
expect(slug).toBe('john-jonhson');
});
});
test('one duplication', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John.').then((slug) => {
expect(slug).toBe('john-1');
});
});
test('multiple duplications', () => {
expect.assertions(1);
return generateSlug(MockUser, 'John Jonhson Jr.').then((slug) => {
expect(slug).toBe('john-jonhson-jr-2');
});
});
});
MockUser
returns a user if the generated slug
does match a value from the slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
array.
MockUser
returns null
if the generated slug
does not match any value from the slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
array.
Think of this slugs
array as an imitation of MongoDB - our "database" has 3 users with the slugs john-jonhson-jr
, john-jonhson-jr-1
, and john
.
This setup for a "database" may look confusing, and it is. Let's discuss 2 examples to better understand the setup.
Take a look at the no duplication
test. Remember that generateSlug(Model, name)
generated the slug
john-johnson
for a MockUser
with the name
John Johnson
. Since john-johnson
does not match any value from the slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
array, MockUser
returns Promise.resolve(null)
. This means that in our "database", there is no user with the john-johnson
slug. Thus, the following code gets executed inside generateSlug()
:
if (!user) {
return origSlug;
}
If user does not exist, origSlug
is indeed original and becomes our user's slug
.
Take a look at the one duplication
test. Again, this method generates the slug john
. Since john
does match a value in the slugs: ['john-jonhson-jr', 'john-jonhson-jr-1', 'john']
array, instead of Promise.resolve(null)
, MockUser
returns Promise.resolve({ id: 'id' })
. Thus, the following code inside generateSlug()
gets executed:
return createUniqueSlug(Model, origSlug, 1);
origSlug
is not original, so the createUniqueSlug()
function adds -1
to the john
slug, thereby outputting john-1
.
Alright, now that you know how tests work - it's time for testing.
To run Jest, add this extra command to our scripts
in package.json
:
"test": "jest"
Go to terminal and run yarn test
. Jest will generate the following report:
From this jest report, we see that our test suite called slugify
passed. This suite consists of three tests: no duplication
, one duplication
, and multiple duplications
.
To see how the report looks when the test fails, go to server/utils/slugify.js
. Find line 24:
return createUniqueSlug(Model, slug, count + 1);
Modify this line to be:
return createUniqueSlug(Model, slug, count + 2);
Go to terminal and run yarn test
. Jest will generate a new report:
This report shows that the third test called multiple duplications
failed. As a result, the entire test suite failed. Jest specifies the reason for failure:
Expected value to be (using ===):
"john-jonhson-jr-2"
Received:
"john-jonhson-jr-3"
In this case, our generateSlug()
function failed the test because it generated john-jonhson-jr-3
instead of john-jonhson-jr-2
. Don't forget to go back to server/utils/slugify.js
and fix the code to pass all tests.
One final note on Jest - saving reports is easy. We simply modify package.json
as follows.
Edit the test
command in scripts
:
"test": "jest --coverage"
Under the scripts
section, add a new section:
"jest": {
"coverageDirectory": "./.coverage"
}
Go to terminal and run yarn test
. Jest will save the generated report to .coverage/lcov-report/index.html
. Open the index.html
file in your browser to see a summary of the report:
You should add the .coverage
directory to your .gitignore
file to exclude it from your remote repository on Github.
In some situations, integration tests are more appropriate than a group of unit tests. In our app, we theoretically can write an integration test that simultaneously tests static CRUD methods in our Book or User model.
One of our book readers (@YuriGor) suggested test that tests entire static method User.signInOrSignUp()
. You can find this test hosted on GithubGist. Here is test:
const mongoose = require('mongoose');
const User = require('../../../server/models/User');
describe('slugify', () => {
beforeAll(async () => {
await mongoose.connect(global.__MONGO_URI__);
console.log('connected');
});
afterAll(async () => {
await mongoose.disconnect();
console.log('disconnected');
});
test('no duplication', async () => {
expect.assertions(1);
await User.remove();
const user = await User.signInOrSignUp({
googleId: 'test1',
email<EMAIL_ADDRESS>googleToken: { accessToken: 'test1', refreshToken: 'test1' },
displayName: 'Test Name',
avatarUrl: 'test1',
});
expect(user.slug).toBe('test-name');
});
});
This test
User.signInOrSignUp()
,user
object has proper value for parameter slug
,In this subsection, we will set up simple logging with Winston.
Winston logging allows us categorize server logs by priority. Why is this useful? With a logger such as Winston, we can output a particular level of messages in development mode but not in production mode.
For example, we may want to see error and warning messages in our local app (locally, or in development mode, you see server logs in your terminal) but only see error messages in the logs of our deployed app (in production mode, you log in into your remote server). Winston helps us achieve exactly that.
Once we set up Winston logger, instead of using console.log()
for all server logs, we can make some messages more important. For example, errors messages (logger.error()
) are more important than non-critical info messages (logger.info()
).
You may use some of your info messages for debugging. In that case, you should not log these messages on your production server, but you also don't want to delete them manually before each deploy. You can achive that by configuring Winston to output messages with certain level of importance.
Before we use Winston logger, we should configure it using the winston.createLogger()
method. We put this custom configuration into our server/logs.js
file. In this book, for the sake of keeping things simple, we specify only 3 options. See the official docs for the entire list of options. Our custom logger looks like:server/logs.js
:
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(winston.format.splat(), winston.format.simple()),
transports: [new winston.transports.Console()],
});
module.exports = logger;
In our custom logger, we specified 3 options:
level
option. This option allows you to specify the message level. The logger will only output messages of a specified level and higher. Here, we simply said the following: in production mode, only log messages with the level info
or higher; in development mode, only log messages with the level debug
or higher. There are 6 levels of messages. Here is a list ordered from highest (0) to lowest priority (5):
{
error: 0,
warn: 1,
info: 2,
verbose: 3,
debug: 4,
silly: 5
}
It's clear now that in production mode, we want to see error
, warn
, and info
messages, but we don't want to see verbose
, debug
, and silly
ones. In development mode, we will also see debug
and verbose
messages. The debug
messages are handy if you debug often and occasionaly forget to delete a debug code when preparing your app for production.
format
option. This option allows you to choose the format of your output. By default, you should choose a simple format winston.format.simple()
, which will output ${info.level}: ${info.message} JSON.stringify({ ...rest })
. However, in addition to simple format, we add splat
format to account for following exception. In Winston logger, if object that you like to print has properties level
or message
- these properties won't be printed since level
and message
are reserved (read more here). To be able to print level
and message
properties of an object, we have to use so called splat
format in addition to simple format.
transports
option. This option allows you to send and save logs to a particular file. However, we won't save logs to a file. We will only output messages to our terminal with the [new winston.transports.Console()]
option.
We are almost ready to test. Open the server/app.js
file and find this line:
console.log(`> Ready on ${ROOT_URL}`); // eslint-disable-line no-console
Replace the line with:
logger.info(`> Ready on ${ROOT_URL}`);
Remember to import our custom logger by adding import logger from './logs';
to the import section of the server/app.js
file.
Start your app with yarn dev
and look at the terminal's output:
If you see a similar output, good job - you configured your custom logger! Now the debugging process will be more productive.
For debugging, use logger.debug()
. For errors, use logger.error()
. Later in this chapter, we will have an opportunity to use our logger for messages of different levels.
In the next section, we will add transactional emails to our app, and we will use our custom logger to output errors in some methods (for example, inside our insertTemplates()
method).
In this section, we'll add transactional emails to our app by using AWS SES.
Our open source project for our Builder Book app has multiple transactional emails. However, in this book, we will show you how to send only one transactional email - the welcome email to users after they sign up. You can follow this welcome email example to create any other transactional email your app needs.
Your package.json
file should have two dependencies that we need. The first one is aws-sdk
, which we'll use to access AWS SES API. The second one is lodash
(introduced in Chapter 3), which we'll use to add variables to our email. For example, we will add a user name variable to address a user by his/her name in our welcome email. If you ran yarn
on the 4-begin
codebase, then you already installed these dependencies and are ready to go. If not, run yarn
now.
Sign up for AWS and navigate to AWS SES.
In this subsection, we want to achieve the following:
.env
Below, we walk you through each step.
Go to Email Addresses
in your SES dashboard. Check the snapshot below to find the link on your dashboard:
Click the dark blue button Verify a New Email Address
. Follow the instructions. Once your email address is verified, you can use it to send emails. You will be able to send test emails from the SES dashboard, as well as from your app using SES API.
Important note: it may take some time for AWS SES to verify your email. Our app will use this verified email as from
email address for transactional emails.
Also note that all new AWS SES accounts are placed in the Amazon SES sandbox, which has limits on which addresses you can send mail from and send mail to. These limits, and instructions on how to remove your account from the sandbox, are described here in the official AWS SES docs.
You may see warnings about these limits in your SES control panel, but for going through this book, you do not need to upgrade from a sandbox account.
To access AWS API from our app, we need to generate an access key and secret access key at Security Credentials. Follow the snapshot below to access your Security Credentials:
Open the section "Access keys (access key ID and secret access key)" and click the dark blue button Create New Access Key
. Copy your access key and secret access key. Keep them in a safe place! After you create your keys, you won't be able to find or edit your secret access key. If you lose your secret access key, you'll have to create a new pair of keys.
Take your newly generated Amazon_accessKeyId
, Amazon_secretAccessKey
, and verified email address EMAIL_SUPPORT_FROM_ADDRESS
. Also select an Amazon_region
for your account (for example, us-east-1
, us-west-2
, or eu-west-1
). Open your .env
file and paste your values:
MONGO_URL_TEST="XXXXXXXXXX"
Google_clientID="XXXXXXXXXX"
Google_clientSecret="XXXXXXXXXX"
Amazon_accessKeyId="XXXXXXXXXX"
Amazon_secretAccessKey="XXXXXXXXXX"
Amazon_region="XXXXXXXXXX"
<EMAIL_ADDRESS>Done!
Note that we us-east-1
is our particular region for AWS SES, but you can configure AWS SES with the specific region for your account.
Also note that you can pass global configurations to AWS as well as unique configurations to a particular AWS service. The latter can be done by following the AWS docs.
In the next sections, we'll write a sendEmail()
function, create a welcome email, and update our User model to send a welcome email to every signed-up user.
We will use the ses.sendEmail()
API method to send emails. Before we jump to it, let's configure our aws.SES()
service. Take a look at the proper syntax in the official docs for AWS SES API. We specify region
, accessKeyId
, and secretAccessKey
. We use the dotenv
package to manage environmental variables and retrieve our keys from .env
using process.env
. Let's place code that configures AWS to server/aws.js
file:server/aws.js
:
aws.config.update({
region: process.env.Amazon_region,
accessKeyId: process.env.Amazon_accessKeyId,
secretAccessKey: process.env.Amazon_secretAccessKey,
});
Next, we initialize SES service with the latest apiVersion
:
const ses = new aws.SES({ apiVersion: 'latest' });
AWS SES offers multiple options for ses.sendEmail()
API method. For example, ToAddresses
, CcAddresses
, BccAddresses
, ReplyToAddresses
, Source
, Subject
, Body
and more.
Let's pass 6 parameters to ses.sendEmail
method. However, if we choose to, when we actually pass parameters to sendEmail
method (sendEmail
calls ses.sendEmail
) we may choose to pass only a subset of parameters.
We will use: Source
(let's call it options.from
), ToAddresses
(options.to
, nested in the Destination
array), CcAddresses
(options.cc
, nested in the Destination
array), Data
(options.subject
, nested in the Message>Subject
array), another Data
(options.body
, nested in the Message>Body>Html
array), and ReplyToAddresses
(options.replyTo
). The names of the options are self-explanatory. We get:server/aws.js
:
ses.sendEmail(
{
Source: options.from,
Destination: {
CcAddresses: options.cc,
ToAddresses: options.to,
},
Message: {
Subject: {
Data: options.subject,
},
Body: {
Html: {
Data: options.body,
},
},
},
ReplyToAddresses: options.replyTo,
},
)
Time to put it all these code snippets together: configuration, initialization of a new service with the latest version of AWS SES API, and the ses.sendEmail()
API method with 6 options.
Make the sendEmail()
function return a Promise. If err
is returned after ses.sendEmail()
is called - the Promise returns reject(err)
. If info
is returned without err
- the Promise returns resolve(info)
:server/aws.js
:
const aws = require('aws-sdk');
function sendEmail(options) {
aws.config.update({
region: process.env.Amazon_region,
accessKeyId: process.env.Amazon_accessKeyId,
secretAccessKey: process.env.Amazon_secretAccessKey,
});
const ses = new aws.SES({ apiVersion: 'latest' });
return new Promise((resolve, reject) => {
ses.sendEmail(
{
Source: options.from,
Destination: {
CcAddresses: options.cc,
ToAddresses: options.to,
},
Message: {
Subject: {
Data: options.subject,
},
Body: {
Html: {
Data: options.body,
},
},
},
ReplyToAddresses: options.replyTo,
},
(err, info) => {
if (err) {
reject(err);
} else {
resolve(info);
}
},
);
});
}
module.exports = sendEmail;
The full list of options and rules for the ses.sendEmail()
API method is in the official AWS SES docs.
You may noticed that we did not write any code that sends a request from our server to AWS SES server. Package aws-sdk
does it for us, the same way passport
package sends request from our server to Google OAuth servers. In Chapters 6 and 8, we will add Github and Mailchimp integrations to our app respectively. For Github and Mailchimp integrations, unlike for Google OAuth and AWS SES, we will explicitly write code that sends server-to-server POST request.
If you like to practice more on how to integrate app with AWS SES, check out our tutorial at codeburst.io.
This subsection is a detour from transactional emails. In this book, we make a detour from time to time. We do so only when we think the detour is important to learn and its location is logical.
This detour subsection will not require you to modify any of your app's code.
Let's talk about exporting and importing modules.
Consider function sendEmail()
.
If our file server/aws.js
had multiple functions to export, we would export sendEmail()
like this:
function sendEmail(options) {
// some code
};
function createTemplate(options) {
// some code
};
export.sendEmail = sendEmail;
The import code may look like:
const { sendEmail } = require('../aws.js');
However, in our case, sendEmail()
is the only function that we export from server/aws.js
, so we specify it by using default export syntax, module.exports
, in our export command:
function sendEmail(options) {
// some code
};
module.exports = sendEmail;
A side note. If we had used babel-node
to compile server code, we could've used newer ES6 syntax and replaced module.exports
with export default
. All client code is automatically compiled by Next.js that uses babel
internally. That means that inside client code you may and do use export
instead of export.X = X
; export default
instead of module.exports
; import
instead of require
.
The import code in the case of default export will be:
const sendEmail = require('../aws');
If you'd like to give this imported function a different name (say you want to give it a more informative name in the new context), you can simply do:
const differentName = require('../aws');
In this book, we build a relatively simple app and aim to keep one function per file, give that function an informative name. We will use default export/import (module.exports = sendEmail;
and const { sendEmail } = require('../aws.js');
) more frequently than standard export/import (export.sendEmail = sendEmail;
and const { sendEmail } = require('../aws.js')
).
In situations where you need to use multiple API methods and maintain good modularity and readibility in your code, we recommend creating a new directory.
Let's say you have 2 AWS SES methods: sendEmail()
and createTemplate()
. We would create a server/aws
folder and place 3 files in it: sendEmail.js
(with default export of sendEmail
), createTemplate.js
(with default export of createTemplate
) and index.js
. The latter file would contain:server/aws/index.js
:
const sendEmail = require('../aws/sendEmail');
const userApi = require('../aws/createTemplate');
export.sendEmail = sendEmail;
export.createTemplate = createTemplate;
Now you can import sendEmail() with:
const { sendEmail } = require('../aws')
You can import all functions with:
const aws = require('../aws');
and access functions as aws.sendEmail
and aws.createTemplate
.
Or you could import all functions like this:
const { sendEmail, createTemplate } = require('../aws');
Note that we import function(s) from ../aws
instead of ../aws/index.js
.
Back to transactional emails. In this subsection, we'll write an email template for our welcome email.
AWS SES has an API method to create templates (i.e. pre-written emails, such as a personalized welcome email). The method is called ses.createTemplate()
and is similar to ses.sendEmail()
. Read more about it here.
But in this book, we will not use ses.createTemplate()
. We will write and add email templates to our code (hardcode it). After our app starts, it will save hardcoded email templates to our database, where we can edit the templates manually (if needed).
Saving templates to our database will ultimately save us time. If we don't save email templates to our database and decide to modify these templates, we would have to edit code and redeploy our app. Editing templates in our database does not require editing code and redeploying our app.
We will create a new Mongoose model called EmailTemplate, and we'll insert a document in our EmailTemplate collection when our app's server starts. Later, when a user signs up in our app, we will send that user a welcome email. The welcome email will be stored in our database as a document in the EmailTemplate collection.
Recall how we created our User model using Mongoose in Chapter 2.server/models/User.js
:
const mongoose = require('mongoose');
const { Schema } = mongoose;
const mongoSchema = new Schema({
googleId: {
type: String,
required: true,
unique: true,
},
googleToken: {
access_token: String,
refresh_token: String,
token_type: String,
expiry_date: Number,
},
slug: {
type: String,
required: true,
unique: true,
},
// more parameters
});
//some code
const User = mongoose.model('User', mongoSchema);
module.exports = User;
Similar to the User model, the basic EmailTemplate model will be:server/models/EmailTemplate.js
:
const mongoose = require('mongoose');
const { Schema } = mongoose;
const mongoSchema = new Schema({
name: {
type: String,
required: true,
unique: true,
},
subject: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
});
const EmailTemplate = mongoose.model('EmailTemplate', mongoSchema);
module.exports = EmailTemplate;
Go ahead and create a server/models/EmailTemplate.js
file.
As we mentioned above, when the app's server starts, we want the server to insert a new document in the emailtemplates
collection of our database. Let's call this function insertTemplates()
. For now, it's main purpose is to check if a template with a specific name already exists in our database and if not, insert that template as a new document in the emailtemplate
collection.
An example of the insertTemplates()
function looks like:
function insertTemplates() {
const templates = [
{
name: 'welcome',
subject: 'Welcome to builderbook.org',
message: 'some text goes here',
},
];
templates.forEach(async (template) => {
// 1. if template with the same name exists in database - return undefined
// 2. otherwise create EmailTemplate document
});
}
For each template (templates.forEach()
) that we want to insert - we search our database asynchronously (with the async/await
construct) for template(s) with the same name (EmailTemplate.find({ name: template.name })
):
if ((await EmailTemplate.find({ name: template.name }).count()) > 0) {
return;
}
We discussed async/await
in Chapter 3, and I'll cover it more in Chapter 5, where we will use this construct several times. Here, the code pauses on the line with await
until count
is returned. If our method finds a template with the same name, then the count is 1 or more, and the method returns undefined
.
If we don't find a template with the same name (count is 0), we create a document with Mongoose's method create()
: EmailTemplate.create()
.
This document will be created with data inside the const templates = []
array by using the .create(template)
method. This method copies the name, subject, and message of a template in our code to an newly created empty document in our database:
EmailTemplate
.create(template)
.catch((error) => {
logger.error('EmailTemplate insertion error:', error);
});
If EmailTemplate.create()
returns an error, we want to catch it and log it to terminal with logger.error()
. Thanks, Winston!
After putting all parts of the insertTemplates()
function together:
function insertTemplates() {
const templates = [
{
name: 'welcome',
subject: 'Welcome to builderbook.org',
message: 'some text goes here',
},
];
templates.forEach(async (template) => {
if ((await EmailTemplate.find({ name: template.name }).count()) > 0) {
return;
}
EmailTemplate
.create(template)
.catch((error) => {
logger.error('EmailTemplate insertion error:', error);
});
});
}
Let's modify some text in our welcome email, then add and call the insertTemplates()
method inside our EmailTemplate model:server/models/EmailTemplate.js
:
const EmailTemplate = mongoose.model('EmailTemplate', mongoSchema);
function insertTemplates() {
const templates = [
{
name: 'welcome',
subject: 'Welcome to builderbook.org',
message: `<%= userName %>,
<p>
Thanks for signing up for Builder Book!
</p>
<p>
In our books, we teach you how to build complete, production-ready web apps from scratch.
</p>
Kelly & Timur, Team Builder Book
`,
},
];
templates.forEach(async (template) => {
if ((await EmailTemplate.find({ name: template.name }).count()) > 0) {
return;
}
EmailTemplate
.create(template)
.catch((error) => {
logger.error('EmailTemplate insertion error:', error);
});
});
}
insertTemplates();
We are almost done.
We wrote a function that inserts an email template to our database. To send the welcome email, we need another function that retrieves the EmailTemplate document from our database and replaces variables, such user name, with the document's values.
We name this function getEmailTemplate()
. Inside this function, we want to achieve a few things:
_.template()
method to the found template_.template() is a method that replaces any lodash variable (variable enclosed like this: <%= variable %>
) with actual values. This allows us to personalize our emails by including the variable <%= userName %>
in our email template and replacing it with a user displayName
before sending (we define userName: displayName
). Thus, personalization.
Let's convert what we just said in English into JavaScript:
export default async function getEmailTemplate(name, params) {
// 1. await till method finds template by name,
// 2. if method finds no template - return error
// 3. else, apply `_.template()` to subject and message parameters of template
}
Here's how we'll achieve 1-3:
We'll use Mongoose's EmailTemplate.findOne()
method to find a template by name. We use the async/await
construct that we briefly mentioned when discussing the insertTemplates()
method:
const source = await EmailTemplate.findOne({ name });
Our code pauses on the line with await
until the result is returned (either template or null). We describe async/await
with detailed examples in Chapter 5.
If null is returned instead of template !source
, then let's throw an informative error with the format throw new Error('error description')
:
if (!source) {
throw new Error('No EmailTemplates found. Please check that at least one is generated at server startup, restart your server and try again.');
}
According to the throw
behaviour (read offical docs), the thrown error will be caught by the first .catch
method. Later in this section, we will catch this error when modifying the this.create()
method inside our User model (server/models/User.js
).
When the EmailTemplate.findOne()
method returns a template, apply the _.template()
method to the subject and message of template:
return {
message: _.template(source.message)(params),
subject: _.template(source.subject)(params),
};
After combining these three parts of getEmailTemplate()
, we finally get:
async function getEmailTemplate(name, params) {
const source = await EmailTemplate.findOne({ name });
if (!source) {
throw new Error('No EmailTemplates found. Please check that at least one is generated at server startup, restart your server and try again.');
}
return {
message: _.template(source.message)(params),
subject: _.template(source.subject)(params),
};
}
Now that we've discussed and built both insertTemplates()
and getEmailTemplate()
functions, it's time to add them to our basic EmailTemplate model:
Add the insertTemplates()
and getEmailTemplate()
functions to our EmailTemplate model:server/models/EmailTemplate.js
:
const mongoose = require('mongoose');
const _ = require('lodash');
const logger = require('../logs');
const { Schema } = mongoose;
const mongoSchema = new Schema({
name: {
type: String,
required: true,
unique: true,
},
subject: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
});
const EmailTemplate = mongoose.model('EmailTemplate', mongoSchema);
function insertTemplates() {
const templates = [
{
name: 'welcome',
subject: 'Welcome to builderbook.org',
message: `<%= userName %>,
<p>
Thanks for signing up for Builder Book!
</p>
<p>
In our books, we teach you how to build complete, production-ready web apps from scratch.
</p>
Kelly & Timur, Team Builder Book
`,
},
];
templates.forEach(async (template) => {
if ((await EmailTemplate.find({ name: template.name }).count()) > 0) {
return;
}
EmailTemplate
.create(template)
.catch((error) => {
logger.error('EmailTemplate insertion error:', error);
});
});
}
insertTemplates();
async function getEmailTemplate(name, params) {
const source = await EmailTemplate.findOne({ name });
if (!source) {
throw new Error('No EmailTemplates found. Please check that at least one is generated at server startup, restart your server and try again.');
}
return {
message: _.template(source.message)(params),
subject: _.template(source.subject)(params),
};
}
exports.insertTemplates = insertTemplates;
exports.getEmailTemplate = getEmailTemplate;
As always, we remember to add the missing imports ( in this case for lodash
and logger
):
const _ = require('lodash');
const logger = require('../logs');
You may have noticed that we did not export EmailTemplate
. That's because we do not plan to use the EmailTemplate model in any of our code. But we did export the getEmailTemplate()
function, which we will use inside the User model (welcome email) and Book model (purchase email, Chapter 7) to find a corresponding email template.
If start app, we won't see welcome email template inserted to DB. This is because for insertTemplates
to run it needs to referenced explicitly. We can achieve it by requiring insertTemplates
in server/app.js
.
Before we test, open server/app.js
and import insertTemplates
:
const { insertTemplates } = require('./models/EmailTemplate');
Above line auth({ server, ROOT_URL });
, add the following line:
await insertTemplates();
Finally, make the function that starts server asynchronous.
Replace:
app.prepare().then(() => {
With:
app.prepare().then(async () => {
When app starts, insertTemplates
will be called and app won't start till insertion is complete.
We added the above import because Node will execute code inside insertTemplates
only when this function is referenced explicitly. For more information, you read this issue.
Time to test - start your app with yarn dev
and go to the list of collections on your Atlas dashboard.  Click on the test.emailtemplates
collection of test
database. You should see that our welcome email template was added to DB:
Hooray! Upon server start, the app successfully inserts our welcome email template to the database.
In the next subsection, we will update our User model to ensure that every newly created user gets a welcome email.
So far, we've created:
sendEmail()
function that sends an email via AWS SES APIinsertTemplates()
that saves a welcome email template to our databasegetEmailTemplate()
that retrieves the welcome email from our database and passes a value to the <%= userName %>
variableThe last step is to modify the signInOrSignUp()
static method of our User model (see server/models/User.js
). This method either finds an existing user and updates the Google OAuth tokens of that user or creates a new user. We would like to update the part of signInOrSignUp()
that creates a new user, since our app will send a welcome email to newly created users.
async signInOrSignUp()
static method await
for the template
that is returned by our getEmailTemplate()
method:const template = await getEmailTemplate('welcome', {
userName: displayName,
});
We pass userName
to our getEmailTemplate
method so that <%= userName %>
gets an actual value.await
for the sendEmail()
method to send an email. Pass the template
that you got from getEmailTemplate()
to sendEmail()
. To send an email, pass values to the from
and to
parameters.To catch potential errors from sendEmail()
, we use the try/catch
construct in combination with async/await
:
try {
await sendEmail({
from: `Kelly from Builder Book <${process.env.EMAIL_SUPPORT_FROM_ADDRESS}>`,
to: [email],
subject: template.subject,
body: template.message,
});
} catch (err) {
logger.error('Email sending error:', err);
}
A note about errors - logger.error('Email sending error:', err);
outputs errors from sendEmail()
. We catch and output all other errors from our signInOrSignUp()
method in server/google.js
at:
try {
const user = await User.signInOrSignUp({
googleId: profile.id,
email,
googleToken: { accessToken, refreshToken },
displayName: profile.displayName,
avatarUrl,
});
verified(null, user);
} catch (err) {
verified(err);
console.log(err); // eslint-disable-line
}
A note on to: [email]
- though the email is saved in our database as a string, we pass the email as an array with a single string to satisfy an AWS SES API requirement.
Alright, we are done updating the signInOrSignUp()
method!
Modify this method inside the User model at server/models/User.js
. This part of signInOrSignUp()
:
const slug = await generateSlug(this, displayName);
const userCount = await this.find().countDocuments();
const newUser = await this.create({
createdAt: new Date(),
googleId,
email,
googleToken,
displayName,
avatarUrl,
slug,
isAdmin: userCount === 0,
});
return _.pick(newUser, UserClass.publicFields());
becomes:
const slug = await generateSlug(this, displayName);
const userCount = await this.find().countDocuments();
const newUser = await this.create({
createdAt: new Date(),
googleId,
email,
googleToken,
displayName,
avatarUrl,
slug,
isAdmin: userCount === 0,
});
const template = await getEmailTemplate('welcome', {
userName: displayName,
});
try {
await sendEmail({
from: `Kelly from Builder Book <${process.env.EMAIL_SUPPORT_FROM_ADDRESS}>`,
to: [email],
subject: template.subject,
body: template.message,
});
} catch (err) {
logger.error('Email sending error:', err);
}
return _.pick(newUser, UserClass.publicFields());
Remember to add all missing imports to server/models/User.js
:
const sendEmail = require('../aws');
const { getEmailTemplate } = require('./EmailTemplate');
const logger = require('../logs');
Our User model with updated signInOrSignUp()
method:server/model/User.js
:
const mongoose = require('mongoose');
const _ = require('lodash');
const generateSlug = require('../utils/slugify');
const sendEmail = require('../aws');
const { getEmailTemplate } = require('./EmailTemplate');
const logger = require('../logs');
const { Schema } = mongoose;
const mongoSchema = new Schema({
googleId: {
type: String,
required: true,
unique: true,
},
googleToken: {
access_token: String,
refresh_token: String,
token_type: String,
expiry_date: Number,
},
slug: {
type: String,
required: true,
unique: true,
},
createdAt: {
type: Date,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
isAdmin: {
type: Boolean,
default: false,
},
displayName: String,
avatarUrl: String,
isGithubConnected: {
type: Boolean,
default: false,
},
githubAccessToken: {
type: String,
},
});
class UserClass {
static publicFields() {
return ['id', 'displayName', 'email', 'avatarUrl', 'slug', 'isAdmin', 'isGithubConnected'];
}
static async signInOrSignUp({
googleId, email, googleToken, displayName, avatarUrl,
}) {
const user = await this.findOne({ googleId }).select(UserClass.publicFields().join(' '));
if (user) {
const modifier = {};
if (googleToken.accessToken) {
modifier.access_token = googleToken.accessToken;
}
if (googleToken.refreshToken) {
modifier.refresh_token = googleToken.refreshToken;
}
if (_.isEmpty(modifier)) {
return user;
}
await this.updateOne({ googleId }, { $set: modifier });
return user;
}
const slug = await generateSlug(this, displayName);
const userCount = await this.find().countDocuments();
const newUser = await this.create({
createdAt: new Date(),
googleId,
email,
googleToken,
displayName,
avatarUrl,
slug,
isAdmin: userCount === 0,
});
const template = await getEmailTemplate('welcome', {
userName: displayName,
});
try {
await sendEmail({
from: `Kelly from Builder Book <${process.env.EMAIL_SUPPORT_FROM_ADDRESS}>`,
to: [email],
subject: template.subject,
body: template.message,
});
} catch (err) {
logger.error('Email sending error:', err);
}
return _.pick(newUser, UserClass.publicFields());
}
}
mongoSchema.loadClass(UserClass);
const User = mongoose.model('User', mongoSchema);
module.exports = User;
Time to test!
Let's test whether our app sends a welcome email to a new user via AWS SES API.
Run your app with yarn dev
and go to the /login
page. Make sure that the Google account you are about to sign up with has never been used to sign up on the app. If your Google account has already been used, and you still want to use it - go to your Atlas dashboard, click on the test.users
collection, and delete the document corresponding to the Google account you want to use.
After sign up, you will be redirected to the app's dashboard at /
route:
Open your Gmail inbox and find the welcome email. In our example, the email is sent from<EMAIL_ADDRESS>:
Here is our welcome email (the one we composed inside our server code and inserted into our database as a document in the emailtemplates
collection):
If you received an email, then good job on setting up transactional emails!
In the next section, we will build in-app notifications - messages that our app shows a user to communicate success or failure of the user's action. For example, when the Admin user edits a book, we would like our app to show a Saved
success message.
So far in this chapter, we learned how to test with Jest, debug with Winston, and send a transactional welcome email to a new user. In this last section, we will discuss and build a component that shows success or failure messages to our app's user.
In Material Design and Material-UI, an in-app notification is called a snackbar. The top-right version of the simplest snackbar I love snacks
looks like this:
We will build in-app notifications in three steps:
notify()
functionTo create a Notifier component, we'll closely follow Material-UI's example for a simple snackbar.
Before constructing our Notifier component out of Material-UI's Snackbar component, check out the full list of Snackbar component props.
We will use the open
and message
props for the state of our Notifier component. Initially, we want the Snackbar to be closed and message to be an empty string:
state = {
open: false,
message: '',
};
We want the Snackbar to close (set open
to false
) when a user clicks away. We'll do this with the handleSnackbarRequestClose
function:
handleSnackbarRequestClose = () => {
this.setState({
open: false,
message: '',
});
};
We set the message
to an empty string, so any new in-app notification does not inherit a message from the previous in-app notification.
We will point the onClose
prop to the handleSnackbarRequestClose
function:
onClose={this.handleSnackbarRequestClose}
Finally, let's render our Notifier component.
In addition to open
, message
, and onClose
props, we will use 3 more props from this list of options):
anchorOrigin
(specifies location of the Snackbar)autoHideDuration
(specifies duration of the Snackbar in milliseconds)ContentProps
(binds the Snackbar to an element that contains its message
)Notifier's render()
:
render() {
const message = (
<span id="snackbar-message-id" dangerouslySetInnerHTML={{ __html: this.state.message }} />
);
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
message={message}
autoHideDuration={5000}
onClose={this.handleSnackbarRequestClose}
open={this.state.open}
ContentProps={{
'aria-describedby': 'snackbar-message-id',
}}
/>
);
}
You may wonder why we use dangerouslySetInnerHTML={{ __html: this.state.message }}
. The dangerouslySetInnerHTML
option allows you, as the developer of this app, to use HTML code inside the Snackbar's message
prop. This is handy if you want to add a hyperlink to the Notifier's message
.
Create a components/Notifier.js
file and put the Notifier component together using the bits code above:components/Notifier.js
:
import React from 'react';
import Snackbar from '@material-ui/core/Snackbar';
class Notifier extends React.Component {
state = {
open: false,
message: '',
};
handleSnackbarRequestClose = () => {
this.setState({
open: false,
message: '',
});
};
render() {
const message = (
<span id="snackbar-message-id" dangerouslySetInnerHTML={{ __html: this.state.message }} />
);
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
message={message}
autoHideDuration={5000}
onClose={this.handleSnackbarRequestClose}
open={this.state.open}
ContentProps={{
'aria-describedby': 'snackbar-message-id',
}}
/>
);
}
}
export default Notifier;
If you carefully look at our Notifier component's code, you'll see that the Snackbar always stays in a closed state. The initial state has open
set to false
, and there is no event that triggers open
to become true
.
Material-UI's example for a simple Snackbar at least contains a button. Clicking the button sets open
to true
via the handleClick
function:
handleClick = () => {
this.setState({ open: true });
};
In other words, the user triggers Notifier with an action.
Most of our in-app messages will be triggered by successful or failed events. For example, we'll show a success notification when our Admin user successfully changes a book's price or when our Customer user successfully creates a bookmark inside a chapter.
We import our Notifier component into our higher-order component App
at pages/_app.js
, and trigger the Notifier
component to show up with notify()
. We import and use the notify()
function inside any page that shows users an in-app message.
In the following section, we'll introduce the openSnackbar
function that opens (sets open
to true
) the Snackbar and passes a message
to it. We will use the openSnackbar
function to construct a notify()
function - we will then use this notify()
function inside different pages to open our Notifier component.
The openSnackbar
function does two things, both via state
:
open
prop of the Snackbar component to true
openSnackbar = ({ message }) => {
this.setState({ open: true, message });
};
In order to access the openSnackbar()
function from anywhere in the app, we need to set its value to another function (openSnackbarFn
) that is available outside of the Notifier component. Thus:
...
let openSnackbarFn;
...
export function openSnackbar({ message }) {
openSnackbarFn({ message });
}
...
Then, inside the Notifier component, after the component is mounted:
componentDidMount() {
openSnackbarFn = this.openSnackbar;
}
Take the 3 code snippets above and add them to components/Notifier.js
:components/Notifier.js
:
import React from 'react';
import Snackbar from '@material-ui/core/Snackbar';
let openSnackbarFn;
class Notifier extends React.Component {
state = {
open: false,
message: '',
};
componentDidMount() {
openSnackbarFn = this.openSnackbar;
}
handleSnackbarRequestClose = () => {
this.setState({
open: false,
message: '',
});
};
openSnackbar = ({ message }) => {
this.setState({ open: true, message });
};
render() {
const message = (
<span id="snackbar-message-id" dangerouslySetInnerHTML={{ __html: this.state.message }} />
);
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
message={message}
autoHideDuration={5000}
onClose={this.handleSnackbarRequestClose}
open={this.state.open}
ContentProps={{
'aria-describedby': 'snackbar-message-id',
}}
/>
);
}
}
export function openSnackbar({ message }) {
openSnackbarFn({ message });
}
export default Notifier;
We use our exported openSnackbar()
function to define a notify()
function at lib/notifier.js
. Create this file:lib/notifier.js
:
import { openSnackbar } from '../components/Notifier';
export default function notify(obj) {
openSnackbar({ message: obj.message || obj.toString() });
}
Most messages that we will pass to our openSnackbar
function are plain text strings. However, we will use openSnackbar
to display errors, and some of the error objects do not have a message property (there is no obj.message
property inside object). To solve this, we define a notify(obj)
function that takes obj
and passes either obj.message
or stringified obj.toString()
as a message
to the openSnackbar
function.
Throughout our app, we'll use the notify()
function to show success, error, or occasionaly some informational notifications to users.
It's time to see our Notifier component and notify()
function in action.
As a test, let's set up a tooltip on our Index page.
First, import the Notifier component to our App
HOC at pages/_app.js
:
import Notifier from '../components/Notifier';
Then add <Notifier />
to pages/_app.js
like this:
<Container>
{/* ThemeProvider makes the theme available down the React
tree thanks to React context. */}
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Header {...pageProps} />
<Component {...pageProps} />
<Notifier />
</ThemeProvider>
</Container>
Second, import the notify()
function and Button
component from Material-UI to the Index page
(pages/index.js
). And add the following code snippet right after <p>Email: {user.email}</p>
line:
<Button variant="contained" onClick={() => notify('success message')}>
Click me to test notify()
</Button>
You should get:pages/index.js
:
import React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import Button from '@material-ui/core/Button';
import withAuth from '../lib/withAuth';
import notify from '../lib/notifier';
class Index extends React.Component {
static propTypes = {
user: PropTypes.shape({
displayName: PropTypes.string,
email: PropTypes.string.isRequired,
}),
}
static defaultProps = {
user: null,
}
render() {
const { user } = this.props;
return (
<div style={{ padding: '10px 45px' }}>
<Head>
<title>Dashboard</title>
<meta
name="description"
content="List of purchased books."
/>
</Head>
<p>List of purchased books</p>
<p>
Email:
{user.email}
</p>
<Button variant="contained" onClick={() => notify('success message')}>
Click me to test notify()
</Button>
</div>
);
}
}
export default withAuth(Index);
Start your app with yarn dev
and log in. Your Index page
will be:
Click the button:
Success - your in-app notification opens up as expected (and disappears in 5 seconds as well)!
As we create more pages, we will use our notify()
function several times. Mostly for our Admin user: add/edit book, sync book's content with Github. Occasionaly for our Customer: creating a bookmark. Keep an eye out for more of notify()
, especially in Chapters 5 and 6.
In the next chapter (Chapter 5), you will set up internal APIs. You will learn that implementing a data exchange between client and server consists of four steps. For example, data may travel from page
to API method
to Express route
to static method
of some model.
At the end of Chapter 4, your codebase should look like the codebase in 4-end
. The 4-end folder is located at the root of the book
directory inside the builderbook repo.
Compare your codebase and make edits if needed.
You're halfway through the book! If you're enjoying it, please share a quick review. You can update your review at any time.


As many are unsurprised to know, the tech industry is dominated with men. It has always been, and remains that way today. Women make up only one-fourth of the tech industry workforce, and with the Bureau of Labor Statistics predicting that by 2020 there will be 1.4 million computer-science related jobs available in the US, that trend will continue in the future unless we do something about it.
Here are some background stats you may want to know:
But it's not all doom and gloom: here are the rays of hope emerging and hopefully staying:
Not only do women face an unwelcoming environment when it comes to the tech world, they are often saddled with sexual harrassment and lack of support, among other things. It's no wonder, because little girls are taught to like Barbies and rainbows while gadgets and technology are for boys. In this world of gender stereotypes, it's hard for women to come out of their role as "I'm supposed to be teacher and leave the hard stuff for men".
The way to combat this is to go back to the roots. Encourage little girls to get into science and technology (that it's ok to be "geeky" while also playing with Easy Bake Ovens) at a young age. Encourage them to take technology-related courses in school. Encourage them to code because that's bad-ass.