├── calendar
├── after
│ ├── .gitignore
│ ├── createDayElement.js
│ ├── createEventElement.js
│ ├── events.js
│ ├── index.html
│ ├── modal.js
│ ├── package-lock.json
│ ├── package.json
│ ├── renderMonth.js
│ ├── script.js
│ └── styles.css
└── before
│ ├── index.html
│ └── styles.css
├── custom-database
├── .gitignore
├── Table.js
├── Table.test.js
├── commands
│ ├── DeleteCommand.js
│ ├── DeleteCommand.test.js
│ ├── InsertCommand.js
│ ├── InsertCommand.test.js
│ ├── SelectCommand.js
│ ├── SelectCommand.test.js
│ ├── UpdateCommand.js
│ ├── UpdateCommand.test.js
│ ├── WhereCommand.js
│ └── WhereCommand.test.js
├── errors
│ ├── InvalidCommandError.js
│ └── TableDoesNotExistError.js
├── package-lock.json
├── package.json
├── parseCommand.js
├── parsers
│ ├── delete.js
│ ├── delete.test.js
│ ├── insert.js
│ ├── insert.test.js
│ ├── select.js
│ ├── select.test.js
│ ├── update.js
│ ├── update.test.js
│ ├── where.js
│ └── where.test.js
├── script.js
└── utils
│ └── safeParseJSON.js
├── ecommerce
├── after
│ ├── client
│ │ ├── .gitignore
│ │ ├── api.js
│ │ ├── download-links.html
│ │ ├── index.html
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── script.js
│ │ └── styles.css
│ └── server
│ │ ├── .gitignore
│ │ ├── contacts.js
│ │ ├── downloads
│ │ ├── javascript-simplified.txt
│ │ ├── learn-css-today.txt
│ │ └── learn-react-today.txt
│ │ ├── items.json
│ │ ├── mailer.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── sendInBlueApiInstance.js
│ │ └── server.js
└── before
│ ├── download-links.html
│ ├── downloads
│ ├── javascript-simplified.txt
│ ├── learn-css-today.txt
│ └── learn-react-today.txt
│ ├── index.html
│ └── styles.css
├── pictionary-clone
├── after
│ ├── .gitignore
│ ├── client
│ │ ├── DrawableCanvas.js
│ │ ├── index.css
│ │ ├── index.html
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── room.css
│ │ ├── room.html
│ │ └── room.js
│ └── server
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── server.js
└── before
│ ├── index.css
│ ├── index.html
│ ├── room.css
│ └── room.html
├── tooltip
├── after
│ ├── .gitignore
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── script.js
│ ├── styles.css
│ ├── tooltip.js
│ └── utils
│ │ └── addGlobalEventListener.js
└── before
│ ├── index.html
│ ├── script.js
│ └── styles.css
└── trello-clone
├── after
├── .gitignore
├── dragAndDrop.js
├── index.html
├── package-lock.json
├── package.json
├── script.js
├── styles.css
└── utils
│ └── addGlobalEventListener.js
└── before
├── index.html
└── styles.css
/calendar/after/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/calendar/after/createDayElement.js:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns"
2 | import createEventElement from "./createEventElement"
3 | import { addEvent, getEventsForDay } from "./events"
4 | import { openAddEventModal, openViewAllModal } from "./modal"
5 | import renderMonth from "./renderMonth"
6 |
7 | const dayTemplate = document.getElementById("day-template")
8 | export default function createDayElement(date, options = {}) {
9 | const {
10 | isCurrentMonth = true,
11 | isCurrentDay = false,
12 | showWeekName = false,
13 | } = options
14 |
15 | const dayElement = dayTemplate.content
16 | .cloneNode(true)
17 | .querySelector("[data-date-wrapper]")
18 |
19 | if (!isCurrentMonth) {
20 | dayElement.classList.add("non-month-day")
21 | }
22 |
23 | if (showWeekName) {
24 | dayElement.querySelector("[data-week-name]").textContent = format(date, "E")
25 | }
26 |
27 | dayElement
28 | .querySelector("[data-add-event-btn]")
29 | .addEventListener("click", () => {
30 | openAddEventModal(date, event => {
31 | addEvent(event)
32 | renderMonth(date)
33 | })
34 | })
35 |
36 | dayElement
37 | .querySelector("[data-view-more-btn]")
38 | .addEventListener("click", () => {
39 | openViewAllModal(date, getEventsForDay(date).map(createEventElement))
40 | })
41 |
42 | const dayNumberElement = dayElement.querySelector("[data-day-number]")
43 | dayNumberElement.textContent = date.getDate()
44 | if (isCurrentDay) {
45 | dayNumberElement.classList.add("active")
46 | }
47 |
48 | const eventContainer = dayElement.querySelector("[data-event-container]")
49 | eventContainer.innerHTML = ""
50 | getEventsForDay(date).forEach(event => {
51 | eventContainer.append(createEventElement(event))
52 | })
53 |
54 | return dayElement
55 | }
56 |
--------------------------------------------------------------------------------
/calendar/after/createEventElement.js:
--------------------------------------------------------------------------------
1 | import { format, parse } from "date-fns"
2 | import { openEditEventModal } from "./modal"
3 | import renderMonth from "./renderMonth"
4 | import { removeEvent, updateEvent } from "./events"
5 |
6 | export default function createEventElement(event) {
7 | const element = event.isAllDay
8 | ? createAllDayEventElement(event)
9 | : createTimedEventElement(event)
10 |
11 | element.addEventListener("click", () => {
12 | openEditEventModal(
13 | event,
14 | updatedEvent => {
15 | updateEvent(updatedEvent)
16 | renderMonth(updatedEvent.date)
17 | },
18 | deletedEvent => {
19 | removeEvent(deletedEvent)
20 | renderMonth(deletedEvent.date)
21 | }
22 | )
23 | })
24 |
25 | return element
26 | }
27 |
28 | const allDayEventTemplate = document.getElementById("all-day-event-template")
29 | function createAllDayEventElement(event) {
30 | const element = allDayEventTemplate.content
31 | .cloneNode(true)
32 | .querySelector("[data-event]")
33 |
34 | element.classList.add(event.color)
35 | element.querySelector("[data-name]").textContent = event.name
36 | return element
37 | }
38 |
39 | const timedEventTemplate = document.getElementById("timed-event-template")
40 | function createTimedEventElement(event) {
41 | const element = timedEventTemplate.content
42 | .cloneNode(true)
43 | .querySelector("[data-event]")
44 | element.querySelector("[data-name]").textContent = event.name
45 | element.querySelector("[data-color]").classList.add(event.color)
46 | element.querySelector("[data-time]").textContent = format(
47 | parse(event.startTime, "HH:mm", event.date),
48 | "h:mmaaa"
49 | )
50 | return element
51 | }
52 |
--------------------------------------------------------------------------------
/calendar/after/events.js:
--------------------------------------------------------------------------------
1 | import { isSameDay, parseISO } from "date-fns"
2 |
3 | const EVENTS_KEY = "CALENDAR.events"
4 |
5 | let events = (JSON.parse(localStorage.getItem(EVENTS_KEY)) || []).map(event => {
6 | return { ...event, date: parseISO(event.date) }
7 | })
8 |
9 | export function addEvent(event) {
10 | events.push(event)
11 | save()
12 | }
13 |
14 | export function updateEvent(event) {
15 | events = events.map(e => {
16 | if (e.id === event.id) return event
17 | return e
18 | })
19 | save()
20 | }
21 |
22 | export function removeEvent(event) {
23 | events = events.filter(e => e.id !== event.id)
24 | save()
25 | }
26 |
27 | export function getEventsForDay(date) {
28 | return events.filter(event => isSameDay(event.date, date)).sort(compareEvents)
29 | }
30 |
31 | function compareEvents(eventA, eventB) {
32 | if (eventA.isAllDay && eventB.isAllDay) {
33 | return 0
34 | } else if (eventA.isAllDay) {
35 | return -1
36 | } else if (eventB.isAllDay) {
37 | return 1
38 | } else {
39 | return (
40 | eventTimeToNumber(eventA.startTime) - eventTimeToNumber(eventB.startTime)
41 | )
42 | }
43 | }
44 |
45 | function eventTimeToNumber(time) {
46 | return parseFloat(time.replace(":", "."))
47 | }
48 |
49 | function save() {
50 | localStorage.setItem(EVENTS_KEY, JSON.stringify(events))
51 | }
52 |
--------------------------------------------------------------------------------
/calendar/after/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Calendar
10 |
11 |
12 |
23 |
24 |
28 |
29 |
30 | 4/8/21
31 |
32 |
33 |
34 |
38 |
73 |
74 |
75 |
76 |
85 |
86 |
87 |
88 |
91 |
92 |
93 |
94 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/calendar/after/modal.js:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns"
2 | import { v4 as uuidV4 } from "uuid"
3 |
4 | const modal = document.querySelector("[data-modal]")
5 | const modalBody = document.querySelector("[data-modal-body]")
6 | const overlay = document.querySelector("[data-overlay]")
7 | overlay.addEventListener("click", closeModal)
8 | document.addEventListener("keydown", e => {
9 | if (e.key === "Escape") closeModal()
10 | })
11 |
12 | const viewAllModalTemplate = document.getElementById("view-all-events-template")
13 | export function openViewAllModal(date, eventElements) {
14 | const modalBody = viewAllModalTemplate.content.cloneNode(true)
15 | modalBody.querySelector("[data-title]").textContent = format(date, "M/d/yyyy")
16 | eventElements.forEach(event => modalBody.append(event))
17 | openModal(modalBody)
18 | }
19 |
20 | const eventModalTemplate = document.getElementById("event-form-template")
21 | export function openAddEventModal(date, saveCallback) {
22 | openModal(getEventFormModalBody({ date }, saveCallback))
23 | }
24 |
25 | export function openEditEventModal(event, saveCallback, deleteCallback) {
26 | openModal(getEventFormModalBody(event, saveCallback, deleteCallback))
27 | }
28 |
29 | function getEventFormModalBody(event, saveCallback, deleteCallback) {
30 | const formModalBody = eventModalTemplate.content.cloneNode(true)
31 | const isNewEvent = event.id == null
32 | formModalBody.querySelector("[data-title]").textContent = isNewEvent
33 | ? "Add Event"
34 | : "Edit Event"
35 | formModalBody.querySelector("[data-date]").textContent = format(
36 | event.date,
37 | "M/d/yyyy"
38 | )
39 |
40 | const form = formModalBody.querySelector("[data-form]")
41 | form.querySelector("[data-save-btn]").textContent = isNewEvent
42 | ? "Add"
43 | : "Update"
44 | const deleteButton = form.querySelector("[data-delete-btn]")
45 | if (isNewEvent) {
46 | deleteButton.remove()
47 | } else {
48 | deleteButton.addEventListener("click", () => {
49 | deleteCallback(event)
50 | closeModal()
51 | })
52 | }
53 |
54 | const nameInput = form.querySelector("[data-name]")
55 | nameInput.value = event.name || ""
56 |
57 | const startTimeInput = form.querySelector("[data-start-time]")
58 | const endTimeInput = form.querySelector("[data-end-time]")
59 | startTimeInput.value = event.startTime
60 | endTimeInput.value = event.endTime
61 |
62 | const allDayCheckbox = form.querySelector("[data-all-day]")
63 | allDayCheckbox.checked = event.isAllDay
64 | startTimeInput.disabled = allDayCheckbox.checked
65 | endTimeInput.disabled = allDayCheckbox.checked
66 | allDayCheckbox.addEventListener("change", () => {
67 | startTimeInput.disabled = allDayCheckbox.checked
68 | endTimeInput.disabled = allDayCheckbox.checked
69 | })
70 | startTimeInput.addEventListener("change", () => {
71 | endTimeInput.min = startTimeInput.value
72 | })
73 |
74 | const colorRadio = form.querySelector(`[data-color][value="${event.color}"]`)
75 | if (colorRadio) colorRadio.checked = true
76 |
77 | form.addEventListener("submit", e => {
78 | e.preventDefault()
79 |
80 | const isAllDay = allDayCheckbox.checked
81 |
82 | saveCallback({
83 | id: event.id || uuidV4(),
84 | name: nameInput.value,
85 | date: event.date,
86 | isAllDay,
87 | startTime: isAllDay ? undefined : startTimeInput.value,
88 | endTime: isAllDay ? undefined : endTimeInput.value,
89 | color: form.querySelector("[data-color]:checked").value,
90 | })
91 |
92 | closeModal()
93 | })
94 |
95 | return formModalBody
96 | }
97 |
98 | function openModal(bodyContents) {
99 | modalBody.innerHTML = ""
100 | modalBody.append(bodyContents)
101 | modal.classList.add("show")
102 | }
103 |
104 | function closeModal() {
105 | modal.classList.remove("show")
106 | }
107 |
--------------------------------------------------------------------------------
/calendar/after/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "current-project",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "snowpack dev",
8 | "build": "snowpack build"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "snowpack": "^3.3.5"
15 | },
16 | "dependencies": {
17 | "date-fns": "^2.21.1",
18 | "uuid": "^3.4.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/calendar/after/renderMonth.js:
--------------------------------------------------------------------------------
1 | import {
2 | startOfMonth,
3 | startOfWeek,
4 | endOfMonth,
5 | endOfWeek,
6 | eachDayOfInterval,
7 | isSameMonth,
8 | isSameDay,
9 | format,
10 | } from "date-fns"
11 | import createDayElement from "./createDayElement"
12 |
13 | const daysContainer = document.querySelector("[data-calendar-days]")
14 | export default function renderMonth(monthDate) {
15 | document.querySelector("[data-month-title]").textContent = format(
16 | monthDate,
17 | "MMMM yyyy"
18 | )
19 | const dayElements = getCalendarDates(monthDate).map((date, index) => {
20 | return createDayElement(date, {
21 | isCurrentMonth: isSameMonth(monthDate, date),
22 | isCurrentDay: isSameDay(Date.now(), date),
23 | showWeekName: index < 7,
24 | })
25 | })
26 | daysContainer.innerHTML = ""
27 | dayElements.forEach(element => daysContainer.append(element))
28 | dayElements.forEach(fixEventOverflow)
29 | }
30 |
31 | export function fixEventOverflow(dateContainer) {
32 | const eventContainer = dateContainer.querySelector("[data-event-container]")
33 | const viewMoreButton = dateContainer.querySelector("[data-view-more-btn]")
34 | const events = eventContainer.querySelectorAll("[data-event]")
35 | viewMoreButton.classList.add("hide")
36 | events.forEach(event => event.classList.remove("hide"))
37 | for (let i = events.length - 1; i >= 0; i--) {
38 | if (dateContainer.scrollHeight <= dateContainer.clientHeight) break
39 | events[i].classList.add("hide")
40 | viewMoreButton.classList.remove("hide")
41 | viewMoreButton.textContent = `+ ${events.length - i} More`
42 | }
43 | }
44 |
45 | function getCalendarDates(date) {
46 | const firstWeekStart = startOfWeek(startOfMonth(date), { weekStartsOn: 1 })
47 | const lastWeekStart = endOfWeek(endOfMonth(date), { weekStartsOn: 1 })
48 | return eachDayOfInterval({ start: firstWeekStart, end: lastWeekStart })
49 | }
50 |
--------------------------------------------------------------------------------
/calendar/after/script.js:
--------------------------------------------------------------------------------
1 | import { addMonths } from "date-fns"
2 | import renderMonth, { fixEventOverflow } from "./renderMonth"
3 |
4 | let selectedMonth = Date.now()
5 | document.querySelector("[data-next-month-btn").addEventListener("click", () => {
6 | selectedMonth = addMonths(selectedMonth, 1)
7 | renderMonth(selectedMonth)
8 | })
9 |
10 | document.querySelector("[data-prev-month-btn").addEventListener("click", () => {
11 | selectedMonth = addMonths(selectedMonth, -1)
12 | renderMonth(selectedMonth)
13 | })
14 |
15 | document.querySelector("[data-today-btn").addEventListener("click", () => {
16 | selectedMonth = Date.now()
17 | renderMonth(selectedMonth)
18 | })
19 |
20 | let resizeTimeout
21 | window.addEventListener("resize", () => {
22 | if (resizeTimeout) clearTimeout(resizeTimeout)
23 | resizeTimeout = setTimeout(() => {
24 | document.querySelectorAll("[data-date-wrapper]").forEach(fixEventOverflow)
25 | }, 100)
26 | })
27 |
28 | renderMonth(selectedMonth)
29 |
--------------------------------------------------------------------------------
/calendar/after/styles.css:
--------------------------------------------------------------------------------
1 | *, *::before, *::after {
2 | box-sizing: border-box;
3 | font-family: sans-serif;
4 | }
5 |
6 | :root {
7 | --blue-background: hsl(200, 80%, 50%);
8 | --red-background: hsl(0, 75%, 60%);
9 | --green-background: hsl(150, 80%, 30%);
10 | --border-color: #dadce0;
11 | --border-size: 1px;
12 | --day-padding: .25rem;
13 | }
14 |
15 | body {
16 | height: 100vh;
17 | margin: 0;
18 | max-width: 1500px;
19 | margin: 0 auto;
20 | }
21 |
22 | .calendar {
23 | height: 100%;
24 | display: flex;
25 | flex-direction: column;
26 | color: #333;
27 | }
28 |
29 | .header {
30 | padding: 1rem;
31 | width: 100%;
32 | display: flex;
33 | align-items: center;
34 | }
35 |
36 | .header > * {
37 | margin-right: .5rem;
38 | }
39 |
40 | .header > :last-child {
41 | margin-right: 0;
42 | }
43 |
44 | .btn {
45 | background: none;
46 | border: 1px solid var(--border-color);
47 | border-radius: .25rem;
48 | padding: .5rem 1rem;
49 | font-size: 1rem;
50 | cursor: pointer;
51 | transition: background-color 250ms;
52 | color: #333;
53 | }
54 |
55 | .btn:hover {
56 | background-color: #f1f3f4;
57 | }
58 |
59 | .btn.btn-delete {
60 | border-color: hsl(0, 75%, 60%);
61 | background-color: hsl(0, 75%, 95%);
62 | color: hsl(0, 75%, 10%);
63 | }
64 |
65 | .btn.btn-delete:hover {
66 | background-color: hsl(0, 75%, 90%);
67 | }
68 |
69 | .btn.btn-success {
70 | border-color: hsl(150, 80%, 30%);
71 | background-color: hsl(150, 80%, 95%);
72 | color: hsl(150, 80%, 10%);
73 | }
74 |
75 | .btn.btn-success:hover {
76 | background-color: hsl(150, 80%, 90%);
77 | }
78 |
79 | .month-change-btn {
80 | cursor: pointer;
81 | background: none;
82 | border: none;
83 | font-size: 1.25rem;
84 | width: 2rem;
85 | padding: 0;
86 | height: 2rem;
87 | text-align: center;
88 | vertical-align: middle;
89 | border-radius: 100%;
90 | transition: background-color 250ms;
91 | color: #333;
92 | }
93 |
94 | .month-change-btn:hover {
95 | background-color: #f1f3f4;
96 | }
97 |
98 | .month-change-btn:first-child {
99 | margin-right: -.5rem;
100 | }
101 |
102 | .month-title {
103 | font-size: 1.5rem;
104 | font-weight: bold;
105 | }
106 |
107 | .days {
108 | flex-grow: 1;
109 | overflow-y: auto;
110 | display: grid;
111 | grid-template-columns: repeat(7, minmax(0, 1fr));
112 | grid-auto-rows: minmax(100px, 1fr);
113 | background-color: var(--border-color);
114 | gap: var(--border-size);
115 | padding: var(--border-size);
116 | }
117 |
118 | .day {
119 | background-color: white;
120 | padding: var(--day-padding);
121 | overflow: hidden;
122 | display: flex;
123 | flex-direction: column;
124 | }
125 |
126 | .non-month-day {
127 | opacity: .75;
128 | }
129 |
130 | .day-header {
131 | display: flex;
132 | flex-direction: column;
133 | align-items: center;
134 | position: relative;
135 | }
136 |
137 | .week-name {
138 | text-transform: uppercase;
139 | font-size: .75rem;
140 | font-weight: bold;
141 | color: #777;
142 | }
143 |
144 | .day-number {
145 | font-size: .9rem;
146 | width: 1.5rem;
147 | height: 1.5rem;
148 | display: flex;
149 | justify-content: center;
150 | align-items: center;
151 | }
152 |
153 | .day-number.active {
154 | background-color: var(--blue-background);
155 | border-radius: 50%;
156 | color: white;
157 | }
158 |
159 | .day:hover .add-event-btn,
160 | .add-event-btn:focus {
161 | opacity: 1;
162 | }
163 |
164 | .add-event-btn {
165 | opacity: 0;
166 | position: absolute;
167 | background: none;
168 | border: none;
169 | border-radius: 50%;
170 | width: 1.5rem;
171 | height: 1.5rem;
172 | display: flex;
173 | justify-content: center;
174 | align-items: center;
175 | right: 0;
176 | top: 0;
177 | font-size: 1.25rem;
178 | cursor: pointer;
179 | color: #333;
180 | }
181 |
182 | .add-event-btn:hover {
183 | background-color: #f1f3f4;
184 | }
185 |
186 | .event {
187 | display: flex;
188 | align-items: center;
189 | overflow: hidden;
190 | white-space: nowrap;
191 | margin-bottom: .5rem;
192 | cursor: pointer;
193 | background: none;
194 | width: 100%;
195 | border: none;
196 | font-size: 1rem;
197 | padding: 0;
198 | }
199 |
200 | .event:last-child {
201 | margin-bottom: 0;
202 | }
203 |
204 | .all-day-event {
205 | color: white;
206 | padding: .15rem .25rem;
207 | border-radius: .25rem;
208 | }
209 |
210 | .all-day-event .event-name {
211 | overflow: hidden;
212 | }
213 |
214 | .event > * {
215 | margin-right: .5rem;
216 | }
217 |
218 | .event > :last-child {
219 | margin-right: 0;
220 | }
221 |
222 | .event-time {
223 | color: #777;
224 | }
225 |
226 | .color-dot {
227 | border-radius: 50%;
228 | width: .5rem;
229 | height: .5rem;
230 | flex-shrink: 0;
231 | }
232 |
233 | .color-dot.blue, .all-day-event.blue {
234 | background-color: var(--blue-background);
235 | }
236 |
237 | .color-dot.red, .all-day-event.red {
238 | background-color: var(--red-background);
239 | }
240 |
241 | .color-dot.green, .all-day-event.green {
242 | background-color: var(--green-background);
243 | }
244 |
245 | .events-view-more-btn {
246 | border: none;
247 | background: none;
248 | font-weight: bold;
249 | color: #555;
250 | cursor: pointer;
251 | }
252 |
253 | .modal {
254 | position: fixed;
255 | top: 0;
256 | left: 0;
257 | bottom: 0;
258 | right: 0;
259 | display: flex;
260 | justify-content: center;
261 | align-items: center;
262 | pointer-events: none;
263 | }
264 |
265 | .modal.show {
266 | pointer-events: all;
267 | }
268 |
269 | .modal .overlay {
270 | background-color: transparent;
271 | width: 100%;
272 | height: 100%;
273 | position: fixed;
274 | transition: background-color 250ms;
275 | }
276 |
277 | .modal.show .overlay {
278 | background-color: rgba(0, 0, 0, .5);
279 | }
280 |
281 | .modal .modal-body {
282 | background-color: white;
283 | border-radius: .5rem;
284 | padding: 1rem;
285 | z-index: 1;
286 | transform: scale(0);
287 | transition: transform 250ms;
288 | min-width: 300px;
289 | max-width: 95%;
290 | }
291 |
292 | .modal.show .modal-body {
293 | transform: scale(1);
294 | }
295 |
296 | .modal-title {
297 | font-size: 1.5rem;
298 | margin-bottom: 1.5rem;
299 | display: flex;
300 | justify-content: space-between;
301 | align-items: center;
302 | }
303 |
304 | .modal-title > * {
305 | margin-right: .25rem;
306 | }
307 |
308 | .modal-title > :last-child {
309 | margin-right: 0;
310 | }
311 |
312 | .modal-title > small {
313 | color: #555;
314 | }
315 |
316 | .form-group {
317 | display: flex;
318 | flex-direction: column;
319 | margin-bottom: 1rem;
320 | }
321 |
322 | .form-group.checkbox {
323 | flex-direction: row;
324 | align-items: center;
325 | }
326 |
327 | .form-group.checkbox input {
328 | cursor: pointer;
329 | margin-right: 0;
330 | }
331 |
332 | .form-group.checkbox label {
333 | padding-left: .5rem;
334 | cursor: pointer;
335 | }
336 |
337 | .form-group:last-child {
338 | margin-bottom: 0;
339 | }
340 |
341 | .form-group label {
342 | font-weight: bold;
343 | font-size: .8rem;
344 | color: #777;
345 | }
346 |
347 | .form-group input {
348 | padding: .25rem .5rem
349 | }
350 |
351 | .row {
352 | display: flex;
353 | }
354 |
355 | .row > * {
356 | flex-grow: 1;
357 | flex-basis: 0;
358 | margin-right: .5rem;
359 | }
360 |
361 | .row > :last-child {
362 | margin-right: 0;
363 | }
364 |
365 | .row.left > * {
366 | flex-grow: 0;
367 | }
368 |
369 | .sr-only {
370 | visibility: hidden;
371 | height: 0;
372 | width: 0;
373 | display: block;
374 | }
375 |
376 | .color-radio {
377 | position: absolute;
378 | opacity: 0;
379 | left: -9999px;
380 | }
381 |
382 | .color-radio + label::before {
383 | content: '';
384 | display: block;
385 | width: 1.75rem;
386 | height: 1.75rem;
387 | border-radius: .25rem;
388 | cursor: pointer;
389 | opacity: .25;
390 | }
391 |
392 | .color-radio:checked + label::before {
393 | opacity: 1;
394 | }
395 |
396 | .color-radio:focus + label::before {
397 | outline: 1px solid black;
398 | }
399 |
400 | .color-radio[value="blue"] + label::before {
401 | background-color: var(--blue-background);
402 | }
403 |
404 | .color-radio[value="red"] + label::before {
405 | background-color: var(--red-background);
406 | }
407 |
408 | .color-radio[value="green"] + label::before {
409 | background-color: var(--green-background);
410 | }
411 |
412 | .hide {
413 | display: none !important;
414 | }
--------------------------------------------------------------------------------
/calendar/before/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Calendar
9 |
10 |
11 |
12 |
20 |
21 |
22 |
27 |
28 |
31 |
34 |
39 |
40 |
41 |
42 |
47 |
48 |
49 |
54 |
55 |
56 |
61 |
62 |
63 |
68 |
69 |
70 |
75 |
76 |
77 |
82 |
83 |
84 |
88 |
89 |
90 |
94 |
95 |
96 |
100 |
101 |
102 |
106 |
107 |
110 |
113 |
118 |
119 |
120 |
121 |
125 |
126 |
129 |
134 |
139 |
144 |
149 |
150 |
153 |
154 |
155 |
159 |
160 |
161 |
165 |
166 |
167 |
171 |
172 |
173 |
177 |
178 |
179 |
183 |
184 |
185 |
189 |
190 |
191 |
195 |
196 |
197 |
201 |
202 |
203 |
207 |
208 |
209 |
213 |
214 |
217 |
220 |
225 |
226 |
227 |
228 |
232 |
233 |
234 |
238 |
239 |
240 |
244 |
245 |
246 |
249 |
250 |
251 |
255 |
256 |
257 |
261 |
262 |
263 |
267 |
268 |
269 |
273 |
274 |
275 |
279 |
280 |
281 |
285 |
286 |
287 |
291 |
292 |
293 |
297 |
298 |
299 |
303 |
304 |
305 |
306 |
307 |
311 |
312 |
313 | 4/8/21
314 |
317 |
322 |
327 |
332 |
337 |
338 |
339 |
340 |
341 |
Add/Edit Event
342 |
4/8/21
343 |
344 |
379 |
380 |
381 |
--------------------------------------------------------------------------------
/calendar/before/styles.css:
--------------------------------------------------------------------------------
1 | *, *::before, *::after {
2 | box-sizing: border-box;
3 | font-family: sans-serif;
4 | }
5 |
6 | :root {
7 | --blue-background: hsl(200, 80%, 50%);
8 | --red-background: hsl(0, 75%, 60%);
9 | --green-background: hsl(150, 80%, 30%);
10 | --border-color: #dadce0;
11 | --border-size: 1px;
12 | --day-padding: .25rem;
13 | }
14 |
15 | body {
16 | height: 100vh;
17 | margin: 0;
18 | max-width: 1500px;
19 | margin: 0 auto;
20 | }
21 |
22 | .calendar {
23 | height: 100%;
24 | display: flex;
25 | flex-direction: column;
26 | color: #333;
27 | }
28 |
29 | .header {
30 | padding: 1rem;
31 | width: 100%;
32 | display: flex;
33 | align-items: center;
34 | }
35 |
36 | .header > * {
37 | margin-right: .5rem;
38 | }
39 |
40 | .header > :last-child {
41 | margin-right: 0;
42 | }
43 |
44 | .btn {
45 | background: none;
46 | border: 1px solid var(--border-color);
47 | border-radius: .25rem;
48 | padding: .5rem 1rem;
49 | font-size: 1rem;
50 | cursor: pointer;
51 | transition: background-color 250ms;
52 | color: #333;
53 | }
54 |
55 | .btn:hover {
56 | background-color: #f1f3f4;
57 | }
58 |
59 | .btn.btn-delete {
60 | border-color: hsl(0, 75%, 60%);
61 | background-color: hsl(0, 75%, 95%);
62 | color: hsl(0, 75%, 10%);
63 | }
64 |
65 | .btn.btn-delete:hover {
66 | background-color: hsl(0, 75%, 90%);
67 | }
68 |
69 | .btn.btn-success {
70 | border-color: hsl(150, 80%, 30%);
71 | background-color: hsl(150, 80%, 95%);
72 | color: hsl(150, 80%, 10%);
73 | }
74 |
75 | .btn.btn-success:hover {
76 | background-color: hsl(150, 80%, 90%);
77 | }
78 |
79 | .month-change-btn {
80 | cursor: pointer;
81 | background: none;
82 | border: none;
83 | font-size: 1.25rem;
84 | width: 2rem;
85 | padding: 0;
86 | height: 2rem;
87 | text-align: center;
88 | vertical-align: middle;
89 | border-radius: 100%;
90 | transition: background-color 250ms;
91 | color: #333;
92 | }
93 |
94 | .month-change-btn:hover {
95 | background-color: #f1f3f4;
96 | }
97 |
98 | .month-change-btn:first-child {
99 | margin-right: -.5rem;
100 | }
101 |
102 | .month-title {
103 | font-size: 1.5rem;
104 | font-weight: bold;
105 | }
106 |
107 | .days {
108 | flex-grow: 1;
109 | overflow-y: auto;
110 | display: grid;
111 | grid-template-columns: repeat(7, minmax(0, 1fr));
112 | grid-auto-rows: minmax(100px, 1fr);
113 | background-color: var(--border-color);
114 | gap: var(--border-size);
115 | padding: var(--border-size);
116 | }
117 |
118 | .day {
119 | background-color: white;
120 | padding: var(--day-padding);
121 | overflow: hidden;
122 | display: flex;
123 | flex-direction: column;
124 | }
125 |
126 | .non-month-day {
127 | opacity: .75;
128 | }
129 |
130 | .day-header {
131 | display: flex;
132 | flex-direction: column;
133 | align-items: center;
134 | position: relative;
135 | }
136 |
137 | .week-name {
138 | text-transform: uppercase;
139 | font-size: .75rem;
140 | font-weight: bold;
141 | color: #777;
142 | }
143 |
144 | .day-number {
145 | font-size: .9rem;
146 | width: 1.5rem;
147 | height: 1.5rem;
148 | display: flex;
149 | justify-content: center;
150 | align-items: center;
151 | }
152 |
153 | .day-number.active {
154 | background-color: var(--blue-background);
155 | border-radius: 50%;
156 | color: white;
157 | }
158 |
159 | .day:hover .add-event-btn,
160 | .add-event-btn:focus {
161 | opacity: 1;
162 | }
163 |
164 | .add-event-btn {
165 | opacity: 0;
166 | position: absolute;
167 | background: none;
168 | border: none;
169 | border-radius: 50%;
170 | width: 1.5rem;
171 | height: 1.5rem;
172 | display: flex;
173 | justify-content: center;
174 | align-items: center;
175 | right: 0;
176 | top: 0;
177 | font-size: 1.25rem;
178 | cursor: pointer;
179 | color: #333;
180 | }
181 |
182 | .add-event-btn:hover {
183 | background-color: #f1f3f4;
184 | }
185 |
186 | .event {
187 | display: flex;
188 | align-items: center;
189 | overflow: hidden;
190 | white-space: nowrap;
191 | margin-bottom: .5rem;
192 | cursor: pointer;
193 | background: none;
194 | width: 100%;
195 | border: none;
196 | font-size: 1rem;
197 | padding: 0;
198 | }
199 |
200 | .event:last-child {
201 | margin-bottom: 0;
202 | }
203 |
204 | .all-day-event {
205 | color: white;
206 | padding: .15rem .25rem;
207 | border-radius: .25rem;
208 | }
209 |
210 | .all-day-event .event-name {
211 | overflow: hidden;
212 | }
213 |
214 | .event > * {
215 | margin-right: .5rem;
216 | }
217 |
218 | .event > :last-child {
219 | margin-right: 0;
220 | }
221 |
222 | .event-time {
223 | color: #777;
224 | }
225 |
226 | .color-dot {
227 | border-radius: 50%;
228 | width: .5rem;
229 | height: .5rem;
230 | flex-shrink: 0;
231 | }
232 |
233 | .color-dot.blue, .all-day-event.blue {
234 | background-color: var(--blue-background);
235 | }
236 |
237 | .color-dot.red, .all-day-event.red {
238 | background-color: var(--red-background);
239 | }
240 |
241 | .color-dot.green, .all-day-event.green {
242 | background-color: var(--green-background);
243 | }
244 |
245 | .events-view-more-btn {
246 | border: none;
247 | background: none;
248 | font-weight: bold;
249 | color: #555;
250 | cursor: pointer;
251 | }
252 |
253 | .modal {
254 | position: fixed;
255 | top: 0;
256 | left: 0;
257 | bottom: 0;
258 | right: 0;
259 | display: flex;
260 | justify-content: center;
261 | align-items: center;
262 | pointer-events: none;
263 | }
264 |
265 | .modal.show {
266 | pointer-events: all;
267 | }
268 |
269 | .modal .overlay {
270 | background-color: transparent;
271 | width: 100%;
272 | height: 100%;
273 | position: fixed;
274 | transition: background-color 250ms;
275 | }
276 |
277 | .modal.show .overlay {
278 | background-color: rgba(0, 0, 0, .5);
279 | }
280 |
281 | .modal .modal-body {
282 | background-color: white;
283 | border-radius: .5rem;
284 | padding: 1rem;
285 | z-index: 1;
286 | transform: scale(0);
287 | transition: transform 250ms;
288 | min-width: 300px;
289 | max-width: 95%;
290 | }
291 |
292 | .modal.show .modal-body {
293 | transform: scale(1);
294 | }
295 |
296 | .modal-title {
297 | font-size: 1.5rem;
298 | margin-bottom: 1.5rem;
299 | display: flex;
300 | justify-content: space-between;
301 | align-items: center;
302 | }
303 |
304 | .modal-title > * {
305 | margin-right: .25rem;
306 | }
307 |
308 | .modal-title > :last-child {
309 | margin-right: 0;
310 | }
311 |
312 | .modal-title > small {
313 | color: #555;
314 | }
315 |
316 | .form-group {
317 | display: flex;
318 | flex-direction: column;
319 | margin-bottom: 1rem;
320 | }
321 |
322 | .form-group.checkbox {
323 | flex-direction: row;
324 | align-items: center;
325 | }
326 |
327 | .form-group.checkbox input {
328 | cursor: pointer;
329 | margin-right: 0;
330 | }
331 |
332 | .form-group.checkbox label {
333 | padding-left: .5rem;
334 | cursor: pointer;
335 | }
336 |
337 | .form-group:last-child {
338 | margin-bottom: 0;
339 | }
340 |
341 | .form-group label {
342 | font-weight: bold;
343 | font-size: .8rem;
344 | color: #777;
345 | }
346 |
347 | .form-group input {
348 | padding: .25rem .5rem
349 | }
350 |
351 | .row {
352 | display: flex;
353 | }
354 |
355 | .row > * {
356 | flex-grow: 1;
357 | flex-basis: 0;
358 | margin-right: .5rem;
359 | }
360 |
361 | .row > :last-child {
362 | margin-right: 0;
363 | }
364 |
365 | .row.left > * {
366 | flex-grow: 0;
367 | }
368 |
369 | .sr-only {
370 | visibility: hidden;
371 | height: 0;
372 | width: 0;
373 | display: block;
374 | }
375 |
376 | .color-radio {
377 | position: absolute;
378 | opacity: 0;
379 | left: -9999px;
380 | }
381 |
382 | .color-radio + label::before {
383 | content: '';
384 | display: block;
385 | width: 1.75rem;
386 | height: 1.75rem;
387 | border-radius: .25rem;
388 | cursor: pointer;
389 | opacity: .25;
390 | }
391 |
392 | .color-radio:checked + label::before {
393 | opacity: 1;
394 | }
395 |
396 | .color-radio:focus + label::before {
397 | outline: 1px solid black;
398 | }
399 |
400 | .color-radio[value="blue"] + label::before {
401 | background-color: var(--blue-background);
402 | }
403 |
404 | .color-radio[value="red"] + label::before {
405 | background-color: var(--red-background);
406 | }
407 |
408 | .color-radio[value="green"] + label::before {
409 | background-color: var(--green-background);
410 | }
411 |
412 | .hide {
413 | display: none !important;
414 | }
--------------------------------------------------------------------------------
/custom-database/.gitignore:
--------------------------------------------------------------------------------
1 | data
2 | node_modules
3 | coverage
--------------------------------------------------------------------------------
/custom-database/Table.js:
--------------------------------------------------------------------------------
1 | const { v4: uuidV4 } = require("uuid")
2 | const fs = require("fs")
3 | const TableDoesNotExistError = require("./errors/TableDoesNotExistError")
4 | const { reject } = require("rsvp")
5 |
6 | module.exports = class Table {
7 | constructor(tableName) {
8 | this.tableName = tableName
9 | }
10 |
11 | get filePath() {
12 | return `data/${this.tableName}.json`
13 | }
14 |
15 | overwriteTable(data) {
16 | return new Promise((resolve, reject) => {
17 | fs.writeFile(this.filePath, JSON.stringify(data), error => {
18 | if (error) return reject(error)
19 | resolve()
20 | })
21 | })
22 | }
23 |
24 | insertRecord(record) {
25 | const recordWithId = { _id: uuidV4(), ...record }
26 | return new Promise((resolve, reject) => {
27 | this.readData()
28 | .catch(e => {
29 | if (e instanceof TableDoesNotExistError) {
30 | return []
31 | } else {
32 | reject(e)
33 | }
34 | })
35 | .then(data => {
36 | fs.writeFile(
37 | this.filePath,
38 | JSON.stringify([...data, recordWithId]),
39 | error => {
40 | if (error) return reject(error)
41 | resolve(recordWithId)
42 | }
43 | )
44 | })
45 | })
46 | }
47 |
48 | readData() {
49 | return new Promise((resolve, reject) => {
50 | fs.readFile(this.filePath, (error, data) => {
51 | if (error) return reject(new TableDoesNotExistError(this.tableName))
52 |
53 | resolve(JSON.parse(data))
54 | })
55 | })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/custom-database/Table.test.js:
--------------------------------------------------------------------------------
1 | const mock = require("mock-fs")
2 | const fs = require("fs")
3 | const Table = require("./Table")
4 | const TableDoesNotExistError = require("./errors/TableDoesNotExistError")
5 |
6 | describe("#readData", () => {
7 | describe("With nonexisting table name", () => {
8 | beforeEach(() => mock({ data: {} }))
9 | afterEach(mock.restore)
10 |
11 | test("It throws TableDoesNotExistError", async () => {
12 | const table = new Table("table")
13 | await expect(table.readData()).rejects.toThrow(TableDoesNotExistError)
14 | })
15 | })
16 |
17 | describe("With an existing table name", () => {
18 | const data = [
19 | { a: 1, b: 2 },
20 | { a: 3, b: 4 },
21 | ]
22 | beforeEach(() => mock({ data: { "table.json": JSON.stringify(data) } }))
23 | afterEach(mock.restore)
24 |
25 | test("It gets all the data in the table", async () => {
26 | const table = new Table("table")
27 | expect(await table.readData()).toIncludeSameMembers(data)
28 | })
29 | })
30 | })
31 |
32 | describe("#insertRecord", () => {
33 | describe("With nonexisting table name", () => {
34 | beforeEach(() => mock({ data: {} }))
35 | afterEach(mock.restore)
36 |
37 | test("It creates the table and adds the record", async () => {
38 | const table = new Table("table")
39 | const recordToInsert = { a: 1, b: 2 }
40 | const { _id, ...newRecordAttributes } = await table.insertRecord(
41 | recordToInsert
42 | )
43 |
44 | expect(
45 | JSON.parse(fs.readFileSync("data/table.json"))
46 | ).toIncludeSameMembers([{ _id, ...newRecordAttributes }])
47 | expect(_id).toBeDefined()
48 | expect(newRecordAttributes).toEqual(recordToInsert)
49 | })
50 | })
51 |
52 | describe("With an existing table", () => {
53 | const data = [
54 | { a: 1, b: 2 },
55 | { a: 3, b: 4 },
56 | ]
57 | beforeEach(() => mock({ data: { "table.json": JSON.stringify(data) } }))
58 | afterEach(mock.restore)
59 |
60 | test("It adds the record", async () => {
61 | const table = new Table("table")
62 | const recordToInsert = { a: 5, b: 6 }
63 | const { _id, ...newRecordAttributes } = await table.insertRecord(
64 | recordToInsert
65 | )
66 |
67 | expect(
68 | JSON.parse(fs.readFileSync("data/table.json"))
69 | ).toIncludeSameMembers([...data, { _id, ...newRecordAttributes }])
70 | expect(_id).toBeDefined()
71 | expect(newRecordAttributes).toEqual(recordToInsert)
72 | })
73 | })
74 | })
75 |
76 | describe("#overwriteTable", () => {
77 | describe("With nonexisting table name", () => {
78 | beforeEach(() => mock({ data: {} }))
79 | afterEach(mock.restore)
80 |
81 | test("It creates the table and adds the data", async () => {
82 | const table = new Table("table")
83 | const dataToInsert = [
84 | { a: 1, b: 2 },
85 | { a: 3, b: 4 },
86 | ]
87 | await table.overwriteTable(dataToInsert)
88 |
89 | expect(
90 | JSON.parse(fs.readFileSync("data/table.json"))
91 | ).toIncludeSameMembers(dataToInsert)
92 | })
93 | })
94 |
95 | describe("With an existing table name", () => {
96 | const data = [
97 | { a: 1, b: 2 },
98 | { a: 3, b: 4 },
99 | ]
100 | beforeEach(() =>
101 | mock({
102 | data: {
103 | "table.json": JSON.stringify(data),
104 | },
105 | })
106 | )
107 | afterEach(mock.restore)
108 |
109 | test("It overwrite the data", async () => {
110 | const table = new Table("table")
111 | const dataToInsert = [
112 | { a: 5, b: 6 },
113 | { a: 7, b: 8 },
114 | ]
115 | await table.overwriteTable(dataToInsert)
116 |
117 | expect(
118 | JSON.parse(fs.readFileSync("data/table.json"))
119 | ).toIncludeSameMembers(dataToInsert)
120 | })
121 | })
122 | })
123 |
--------------------------------------------------------------------------------
/custom-database/commands/DeleteCommand.js:
--------------------------------------------------------------------------------
1 | const Table = require("../Table")
2 |
3 | module.exports = class DeleteCommand {
4 | constructor({ tableName }) {
5 | this.table = new Table(tableName)
6 | }
7 |
8 | async perform(whereCommand) {
9 | const originalData = await this.table.readData()
10 | let dataToDelete = originalData
11 | if (whereCommand) dataToDelete = whereCommand.perform(dataToDelete)
12 | const dataToKeep = originalData.filter(record => {
13 | return !dataToDelete.includes(record)
14 | })
15 |
16 | await this.table.overwriteTable(dataToKeep)
17 | return dataToDelete.map(record => record._id)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/custom-database/commands/DeleteCommand.test.js:
--------------------------------------------------------------------------------
1 | const DeleteCommand = require("./DeleteCommand")
2 |
3 | describe("With a valid command", () => {
4 | const deleteCommand = new DeleteCommand({})
5 | const data = [
6 | { _id: 1, a: 3 },
7 | { _id: 2, a: 4 },
8 | ]
9 |
10 | test("It deletes all records", async () => {
11 | const readSpy = jest
12 | .spyOn(deleteCommand.table, "readData")
13 | .mockResolvedValue(data)
14 | const writeSpy = jest
15 | .spyOn(deleteCommand.table, "overwriteTable")
16 | .mockResolvedValue()
17 |
18 | expect(await deleteCommand.perform()).toIncludeSameMembers([1, 2])
19 | expect(writeSpy).toHaveBeenCalledWith([])
20 | expect(readSpy).toHaveBeenCalled()
21 |
22 | writeSpy.mockRestore()
23 | readSpy.mockRestore()
24 | })
25 | })
26 |
27 | describe("With a where command", () => {
28 | const deleteCommand = new DeleteCommand({})
29 | const whereCommand = { perform: data => [data[0]] }
30 | const data = [
31 | { _id: 1, a: 3 },
32 | { _id: 2, a: 4 },
33 | ]
34 |
35 | test("It deletes all matching records", async () => {
36 | const readSpy = jest
37 | .spyOn(deleteCommand.table, "readData")
38 | .mockResolvedValue(data)
39 | const writeSpy = jest
40 | .spyOn(deleteCommand.table, "overwriteTable")
41 | .mockResolvedValue()
42 |
43 | expect(await deleteCommand.perform(whereCommand)).toIncludeSameMembers([1])
44 | expect(writeSpy).toHaveBeenCalledWith([{ _id: 2, a: 4 }])
45 | expect(readSpy).toHaveBeenCalled()
46 |
47 | writeSpy.mockRestore()
48 | readSpy.mockRestore()
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/custom-database/commands/InsertCommand.js:
--------------------------------------------------------------------------------
1 | const Table = require("../Table")
2 |
3 | module.exports = class InsertCommand {
4 | constructor({ tableName, record }) {
5 | this.table = new Table(tableName)
6 | this.record = record
7 | }
8 |
9 | async perform() {
10 | return await this.table.insertRecord(this.record)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/custom-database/commands/InsertCommand.test.js:
--------------------------------------------------------------------------------
1 | const InsertCommand = require("./InsertCommand")
2 |
3 | describe("With a record", () => {
4 | const insertCommand = new InsertCommand({ record: { a: 1, b: 2 } })
5 |
6 | test("It inserts the record", async () => {
7 | const spy = jest
8 | .spyOn(insertCommand.table, "insertRecord")
9 | .mockResolvedValue()
10 |
11 | await insertCommand.perform()
12 | expect(spy).toHaveBeenCalledWith(insertCommand.record)
13 |
14 | spy.mockRestore()
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/custom-database/commands/SelectCommand.js:
--------------------------------------------------------------------------------
1 | const Table = require("../Table")
2 | const pick = require("lodash/pick")
3 |
4 | module.exports = class SelectCommand {
5 | constructor({ tableName, columns, allColumns }) {
6 | this.table = new Table(tableName)
7 | this.columns = columns
8 | this.allColumns = allColumns
9 | }
10 |
11 | async perform(whereCommand) {
12 | let data = await this.table.readData()
13 | if (whereCommand) data = whereCommand.perform(data)
14 | if (this.allColumns) return data
15 |
16 | return data.map(object => {
17 | return pick(object, this.columns)
18 | })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/custom-database/commands/SelectCommand.test.js:
--------------------------------------------------------------------------------
1 | const SelectCommand = require("./SelectCommand")
2 |
3 | describe("With all columns selected", () => {
4 | const selectCommand = new SelectCommand({ allColumns: true })
5 | const data = [
6 | { a: 1, b: 2 },
7 | { a: 3, b: 4 },
8 | ]
9 |
10 | test("It return the data as is", async () => {
11 | const spy = jest
12 | .spyOn(selectCommand.table, "readData")
13 | .mockResolvedValue(data)
14 |
15 | expect(await selectCommand.perform()).toIncludeSameMembers(data)
16 | expect(spy).toHaveBeenCalled()
17 |
18 | spy.mockRestore()
19 | })
20 | })
21 |
22 | describe("With individual columns selected", () => {
23 | const selectCommand = new SelectCommand({ columns: ["a", "c"] })
24 | const data = [
25 | { a: 1, b: 2, c: 3 },
26 | { a: 3, b: 4, c: 5 },
27 | ]
28 |
29 | test("It returns only the selected columns", async () => {
30 | const spy = jest
31 | .spyOn(selectCommand.table, "readData")
32 | .mockResolvedValue(data)
33 |
34 | expect(await selectCommand.perform()).toIncludeSameMembers([
35 | { a: 1, c: 3 },
36 | { a: 3, c: 5 },
37 | ])
38 | expect(spy).toHaveBeenCalled()
39 |
40 | spy.mockRestore()
41 | })
42 | })
43 |
44 | describe("With a where command", () => {
45 | const selectCommand = new SelectCommand({ allColumns: true })
46 | const whereCommand = { perform: data => [data[0]] }
47 | const data = [
48 | { a: 1, b: 2 },
49 | { a: 3, b: 4 },
50 | ]
51 |
52 | test("It returns only the values matched by the where command", async () => {
53 | const spy = jest
54 | .spyOn(selectCommand.table, "readData")
55 | .mockResolvedValue(data)
56 |
57 | expect(await selectCommand.perform(whereCommand)).toIncludeSameMembers([
58 | { a: 1, b: 2 },
59 | ])
60 | expect(spy).toHaveBeenCalled()
61 |
62 | spy.mockRestore()
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/custom-database/commands/UpdateCommand.js:
--------------------------------------------------------------------------------
1 | const Table = require("../Table")
2 |
3 | module.exports = class UpdateCommand {
4 | constructor({ tableName, properties }) {
5 | this.table = new Table(tableName)
6 | this.properties = properties
7 | }
8 |
9 | async perform(whereCommand) {
10 | const originalData = await this.table.readData()
11 | let dataToUpdate = originalData
12 | if (whereCommand) dataToUpdate = whereCommand.perform(dataToUpdate)
13 | const updatedRecords = []
14 | const newData = originalData.map(record => {
15 | if (dataToUpdate.includes(record)) {
16 | const newRecord = { ...record, ...this.properties }
17 | updatedRecords.push(newRecord)
18 | return newRecord
19 | }
20 |
21 | return record
22 | })
23 |
24 | await this.table.overwriteTable(newData)
25 | return updatedRecords
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/custom-database/commands/UpdateCommand.test.js:
--------------------------------------------------------------------------------
1 | const UpdateCommand = require("./UpdateCommand")
2 |
3 | describe("With properties", () => {
4 | const updateCommand = new UpdateCommand({ properties: { c: 3 } })
5 | const data = [
6 | { a: 1, b: 2 },
7 | { a: 3, b: 4 },
8 | ]
9 |
10 | test("It updates all records", async () => {
11 | const readSpy = jest
12 | .spyOn(updateCommand.table, "readData")
13 | .mockResolvedValue(data)
14 | const writeSpy = jest
15 | .spyOn(updateCommand.table, "overwriteTable")
16 | .mockResolvedValue()
17 |
18 | const expectedData = [
19 | { a: 1, b: 2, c: 3 },
20 | { a: 3, b: 4, c: 3 },
21 | ]
22 |
23 | expect(await updateCommand.perform()).toIncludeSameMembers(expectedData)
24 | expect(writeSpy).toHaveBeenCalledWith(expectedData)
25 | expect(readSpy).toHaveBeenCalled()
26 |
27 | writeSpy.mockRestore()
28 | readSpy.mockRestore()
29 | })
30 | })
31 |
32 | describe("With a where command", () => {
33 | const updateCommand = new UpdateCommand({ properties: { c: 3 } })
34 | const whereCommand = { perform: data => [data[0]] }
35 | const data = [
36 | { a: 1, b: 2 },
37 | { a: 3, b: 4 },
38 | ]
39 |
40 | test("It updates all matching records", async () => {
41 | const readSpy = jest
42 | .spyOn(updateCommand.table, "readData")
43 | .mockResolvedValue(data)
44 | const writeSpy = jest
45 | .spyOn(updateCommand.table, "overwriteTable")
46 | .mockResolvedValue()
47 |
48 | const expectedData = [
49 | { a: 1, b: 2, c: 3 },
50 | { a: 3, b: 4 },
51 | ]
52 |
53 | expect(await updateCommand.perform(whereCommand)).toIncludeSameMembers([
54 | expectedData[0],
55 | ])
56 | expect(writeSpy).toHaveBeenCalledWith(expectedData)
57 | expect(readSpy).toHaveBeenCalled()
58 |
59 | writeSpy.mockRestore()
60 | readSpy.mockRestore()
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/custom-database/commands/WhereCommand.js:
--------------------------------------------------------------------------------
1 | const isMatch = require("lodash/isMatch")
2 |
3 | module.exports = class WhereCommand {
4 | constructor(conditions) {
5 | this.conditions = conditions
6 | }
7 |
8 | perform(objects) {
9 | return objects.filter(object => {
10 | return isMatch(object, this.conditions)
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/custom-database/commands/WhereCommand.test.js:
--------------------------------------------------------------------------------
1 | const WhereCommand = require("./WhereCommand")
2 |
3 | describe("With one condition", () => {
4 | const whereCommand = new WhereCommand({ a: 1 })
5 | const data = [
6 | { a: 1, b: 2 },
7 | { a: 3, b: 4 },
8 | ]
9 |
10 | test("It returns the data that matches", () => {
11 | expect(whereCommand.perform(data)).toIncludeSameMembers([{ a: 1, b: 2 }])
12 | })
13 | })
14 |
15 | describe("With two condition", () => {
16 | const whereCommand = new WhereCommand({ a: 1, b: 2 })
17 | const data = [
18 | { a: 1, b: 2 },
19 | { a: 1, b: 5 },
20 | { a: 3, b: 4 },
21 | ]
22 |
23 | test("It returns the data that matches", () => {
24 | expect(whereCommand.perform(data)).toIncludeSameMembers([{ a: 1, b: 2 }])
25 | })
26 | })
27 |
28 | describe("With no conditions", () => {
29 | const whereCommand = new WhereCommand()
30 | const data = [
31 | { a: 1, b: 2 },
32 | { a: 1, b: 5 },
33 | { a: 3, b: 4 },
34 | ]
35 |
36 | test("It returns the data as is", () => {
37 | expect(whereCommand.perform(data)).toIncludeSameMembers(data)
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/custom-database/errors/InvalidCommandError.js:
--------------------------------------------------------------------------------
1 | module.exports = class InvalidCommandError extends Error {
2 | constructor(commandString) {
3 | super(commandString)
4 |
5 | this.name = "InvalidCommandError"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/custom-database/errors/TableDoesNotExistError.js:
--------------------------------------------------------------------------------
1 | module.exports = class TableDoesNotExistError extends Error {
2 | constructor(tableName) {
3 | super(tableName)
4 |
5 | this.name = "TableDoesNotExistError"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/custom-database/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "current-project",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "script.js",
6 | "scripts": {
7 | "start": "node script.js",
8 | "test": "jest --coverage"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "jest": {
14 | "setupFilesAfterEnv": [
15 | "jest-extended"
16 | ]
17 | },
18 | "devDependencies": {
19 | "jest": "^26.6.3",
20 | "jest-extended": "^0.11.5",
21 | "mock-fs": "^4.13.0"
22 | },
23 | "dependencies": {
24 | "lodash": "^4.17.21",
25 | "uuid": "^8.3.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/custom-database/parseCommand.js:
--------------------------------------------------------------------------------
1 | const InvalidCommandError = require("./errors/InvalidCommandError")
2 | const parseDeleteCommand = require("./parsers/delete")
3 | const parseInsertCommand = require("./parsers/insert")
4 | const parseSelectCommand = require("./parsers/select")
5 | const parseUpdateCommand = require("./parsers/update")
6 | const parseWhereCommand = require("./parsers/where")
7 |
8 | const parsers = [
9 | parseInsertCommand,
10 | parseSelectCommand,
11 | parseUpdateCommand,
12 | parseDeleteCommand,
13 | ]
14 |
15 | module.exports = async function parseCommand(commandString) {
16 | const command = parsers
17 | .map(parser => parser(commandString))
18 | .find(command => command != null)
19 |
20 | if (command == null) throw new InvalidCommandError(commandString)
21 |
22 | const whereCommand = parseWhereCommand(commandString)
23 | return await command.perform(whereCommand)
24 | }
25 |
--------------------------------------------------------------------------------
/custom-database/parsers/delete.js:
--------------------------------------------------------------------------------
1 | const DeleteCommand = require("../commands/DeleteCommand")
2 |
3 | const DELETE_COMMAND = "DELETE"
4 | const BEFORE_TABLE_COMMAND = "FROM"
5 | const REGEX = new RegExp(
6 | `${DELETE_COMMAND}\\s+${BEFORE_TABLE_COMMAND}\\s+(?\\S+)`
7 | )
8 |
9 | function parseDeleteCommand(commandString) {
10 | const regexMatch = commandString.match(REGEX)
11 | if (regexMatch == null) return
12 |
13 | const tableName = regexMatch.groups.tableName
14 |
15 | return new DeleteCommand({
16 | tableName,
17 | })
18 | }
19 |
20 | module.exports = parseDeleteCommand
21 |
--------------------------------------------------------------------------------
/custom-database/parsers/delete.test.js:
--------------------------------------------------------------------------------
1 | const parseDeleteCommand = require("./delete")
2 |
3 | describe("With a valid command", () => {
4 | const command = "DELETE FROM table"
5 |
6 | test("It returns the correct DeleteCommand", () => {
7 | const deleteCommand = parseDeleteCommand(command)
8 | expect(deleteCommand.table.tableName).toBe("table")
9 | })
10 | })
11 |
12 | describe("With a no table name", () => {
13 | const command = "DELETE FROM"
14 |
15 | test("It returns undefined", () => {
16 | expect(parseDeleteCommand(command)).toBeUndefined()
17 | })
18 | })
19 |
20 | describe("With no DELETE clause", () => {
21 | const command = "FROM table"
22 |
23 | test("It returns undefined", () => {
24 | expect(parseDeleteCommand(command)).toBeUndefined()
25 | })
26 | })
27 |
28 | describe("With no FROM clause", () => {
29 | const command = "DELETE table"
30 |
31 | test("It returns undefined", () => {
32 | expect(parseDeleteCommand(command)).toBeUndefined()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/custom-database/parsers/insert.js:
--------------------------------------------------------------------------------
1 | const InsertCommand = require("../commands/InsertCommand")
2 | const safeParseJSON = require("../utils/safeParseJSON")
3 |
4 | const INSERT_COMMAND = "INSERT"
5 | const BEFORE_TABLE_COMMAND = "INTO"
6 | const REGEX = new RegExp(
7 | `${INSERT_COMMAND}\\s+(?{.*})\\s+${BEFORE_TABLE_COMMAND}\\s+(?\\S+)`
8 | )
9 |
10 | function parseInsertCommand(commandString) {
11 | const regexMatch = commandString.match(REGEX)
12 | if (regexMatch == null) return
13 |
14 | const record = safeParseJSON(regexMatch.groups.record)
15 | if (record == null) return
16 |
17 | const tableName = regexMatch.groups.tableName
18 |
19 | return new InsertCommand({
20 | tableName,
21 | record,
22 | })
23 | }
24 |
25 | module.exports = parseInsertCommand
26 |
--------------------------------------------------------------------------------
/custom-database/parsers/insert.test.js:
--------------------------------------------------------------------------------
1 | const parseInsertCommand = require("./insert")
2 |
3 | describe("With a valid command", () => {
4 | const command = 'INSERT { "a": 1, "b": 2 } INTO table'
5 |
6 | test("It returns correct InsertCommand", () => {
7 | const insertCommand = parseInsertCommand(command)
8 | expect(insertCommand.record).toEqual({ a: 1, b: 2 })
9 | expect(insertCommand.table.tableName).toBe("table")
10 | })
11 | })
12 |
13 | describe("With an invalid record", () => {
14 | const command = "INSERT { afdasdf } INTO table"
15 |
16 | test("It returns undefined", () => {
17 | expect(parseInsertCommand(command)).toBeUndefined()
18 | })
19 | })
20 |
21 | describe("With no table name", () => {
22 | const command = 'INSERT { "a": 1, "b": 2 } INTO'
23 |
24 | test("It returns undefined", () => {
25 | expect(parseInsertCommand(command)).toBeUndefined()
26 | })
27 | })
28 |
29 | describe("With no INSERT clause", () => {
30 | const command = '{ "a": 1, "b": 2 } INTO table'
31 |
32 | test("It returns undefined", () => {
33 | expect(parseInsertCommand(command)).toBeUndefined()
34 | })
35 | })
36 |
37 | describe("With no INTO clause", () => {
38 | const command = 'INSERT { "a": 1, "b": 2 } table'
39 |
40 | test("It returns undefined", () => {
41 | expect(parseInsertCommand(command)).toBeUndefined()
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/custom-database/parsers/select.js:
--------------------------------------------------------------------------------
1 | const SelectCommand = require("../commands/SelectCommand")
2 |
3 | const SELECT_COMMAND = "SELECT"
4 | const BEFORE_TABLE_COMMAND = "FROM"
5 | const EVERYTHING_SELECTOR = "*"
6 | const REGEX = new RegExp(
7 | `${SELECT_COMMAND}\\s+(?.*)\\s+${BEFORE_TABLE_COMMAND}\\s+(?\\S+)`
8 | )
9 |
10 | function parseSelectCommand(commandString) {
11 | const regexMatch = commandString.match(REGEX)
12 | if (regexMatch == null) return
13 |
14 | const columns = regexMatch.groups.columns
15 | .replace(/\s/, "")
16 | .split(",")
17 | .filter(column => column !== "")
18 | if (columns.length === 0) return
19 |
20 | const tableName = regexMatch.groups.tableName
21 |
22 | return new SelectCommand({
23 | tableName,
24 | columns,
25 | allColumns: columns.includes(EVERYTHING_SELECTOR),
26 | })
27 | }
28 |
29 | module.exports = parseSelectCommand
30 |
--------------------------------------------------------------------------------
/custom-database/parsers/select.test.js:
--------------------------------------------------------------------------------
1 | const parseSelectCommand = require("./select")
2 |
3 | describe("With all columns", () => {
4 | const command = "SELECT * FROM table"
5 |
6 | test("It returns correct SelectCommand", () => {
7 | const selectCommand = parseSelectCommand(command)
8 | expect(selectCommand.allColumns).toBeTruthy()
9 | expect(selectCommand.table.tableName).toBe("table")
10 | })
11 | })
12 |
13 | describe("With specific columns", () => {
14 | const command = "SELECT a, b FROM table"
15 |
16 | test("It returns correct SelectCommand", () => {
17 | const selectCommand = parseSelectCommand(command)
18 | expect(selectCommand.columns).toIncludeSameMembers(["a", "b"])
19 | expect(selectCommand.allColumns).toBeFalsy()
20 | expect(selectCommand.table.tableName).toBe("table")
21 | })
22 | })
23 |
24 | describe("With no columns", () => {
25 | const command = "SELECT FROM table"
26 |
27 | test("It returns undefined", () => {
28 | expect(parseSelectCommand(command)).toBeUndefined()
29 | })
30 | })
31 |
32 | describe("With malformed columns", () => {
33 | const command = "SELECT , FROM table"
34 |
35 | test("It returns undefined", () => {
36 | expect(parseSelectCommand(command)).toBeUndefined()
37 | })
38 | })
39 |
40 | describe("With no SELECT clause", () => {
41 | const command = "* FROM table"
42 |
43 | test("It returns undefined", () => {
44 | expect(parseSelectCommand(command)).toBeUndefined()
45 | })
46 | })
47 |
48 | describe("With no FROM clause", () => {
49 | const command = "SELECT * table"
50 |
51 | test("It returns undefined", () => {
52 | expect(parseSelectCommand(command)).toBeUndefined()
53 | })
54 | })
55 |
56 | describe("With no table name", () => {
57 | const command = "SELECT * FROM"
58 |
59 | test("It returns undefined", () => {
60 | expect(parseSelectCommand(command)).toBeUndefined()
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/custom-database/parsers/update.js:
--------------------------------------------------------------------------------
1 | const UpdateCommand = require("../commands/UpdateCommand")
2 | const safeParseJSON = require("../utils/safeParseJSON")
3 |
4 | const UPDATE_COMMAND = "UPDATE"
5 | const BEFORE_TABLE_COMMAND = "IN"
6 | const REGEX = new RegExp(
7 | `${UPDATE_COMMAND}\\s+(?{.*})\\s+${BEFORE_TABLE_COMMAND}\\s+(?\\S+)`
8 | )
9 |
10 | function parseUpdateCommand(commandString) {
11 | const regexMatch = commandString.match(REGEX)
12 | if (regexMatch == null) return
13 |
14 | const properties = safeParseJSON(regexMatch.groups.properties)
15 | if (properties == null) return
16 |
17 | const tableName = regexMatch.groups.tableName
18 |
19 | return new UpdateCommand({
20 | tableName,
21 | properties,
22 | })
23 | }
24 |
25 | module.exports = parseUpdateCommand
26 |
--------------------------------------------------------------------------------
/custom-database/parsers/update.test.js:
--------------------------------------------------------------------------------
1 | const parseUpdateCommand = require("./update")
2 |
3 | describe("With valid command", () => {
4 | const command = 'UPDATE { "a": 1, "b": 2 } IN table'
5 |
6 | test("It returns the correct UpdateCommand", () => {
7 | const updateCommand = parseUpdateCommand(command)
8 | expect(updateCommand.properties).toEqual({ a: 1, b: 2 })
9 | expect(updateCommand.table.tableName).toBe("table")
10 | })
11 | })
12 |
13 | describe("With invalid properties", () => {
14 | const command = "UPDATE { asdfasdf } IN table"
15 |
16 | test("It returns undefined", () => {
17 | expect(parseUpdateCommand(command)).toBeUndefined()
18 | })
19 | })
20 |
21 | describe("With no table name", () => {
22 | const command = 'UPDATE { "a": 1, "b": 2 } IN'
23 |
24 | test("It returns undefined", () => {
25 | expect(parseUpdateCommand(command)).toBeUndefined()
26 | })
27 | })
28 |
29 | describe("With no UPDATE clause", () => {
30 | const command = '{ "a": 1, "b": 2 } IN table'
31 |
32 | test("It returns undefined", () => {
33 | expect(parseUpdateCommand(command)).toBeUndefined()
34 | })
35 | })
36 |
37 | describe("With no IN clause", () => {
38 | const command = 'UPDATE { "a": 1, "b": 2 } table'
39 |
40 | test("It returns undefined", () => {
41 | expect(parseUpdateCommand(command)).toBeUndefined()
42 | })
43 | })
44 |
45 | describe("With no properties", () => {
46 | const command = "UPDATE IN table"
47 |
48 | test("It returns undefined", () => {
49 | expect(parseUpdateCommand(command)).toBeUndefined()
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/custom-database/parsers/where.js:
--------------------------------------------------------------------------------
1 | const WhereCommand = require("../commands/WhereCommand")
2 | const safeParseJSON = require("../utils/safeParseJSON")
3 |
4 | const WHERE_COMMAND = "WHERE"
5 | const REGEX = new RegExp(`${WHERE_COMMAND}\\s+(?{.*})`)
6 |
7 | function parseWhereCommand(commandString) {
8 | const regexMatch = commandString.match(REGEX)
9 | if (regexMatch == null) return
10 |
11 | const conditions = safeParseJSON(regexMatch.groups.conditions)
12 | if (conditions == null) return
13 |
14 | return new WhereCommand(conditions)
15 | }
16 |
17 | module.exports = parseWhereCommand
18 |
--------------------------------------------------------------------------------
/custom-database/parsers/where.test.js:
--------------------------------------------------------------------------------
1 | const parseWhereCommand = require("./where")
2 |
3 | describe("With normal command", () => {
4 | const command = 'SELECT * FROM table WHERE { "a": 1, "b": 2 }'
5 |
6 | test("It returns the correct WhereCommand", () => {
7 | expect(parseWhereCommand(command).conditions).toEqual({ a: 1, b: 2 })
8 | })
9 | })
10 |
11 | describe("With invalid command", () => {
12 | const command = "SELECT * FROM table WHERE { asdfasd }"
13 |
14 | test("It returns undefined", () => {
15 | expect(parseWhereCommand(command)).toBeUndefined()
16 | })
17 | })
18 |
19 | describe("With no conditions", () => {
20 | const command = "SELECT * FROM table WHERE"
21 |
22 | test("It returns undefined", () => {
23 | expect(parseWhereCommand(command)).toBeUndefined()
24 | })
25 | })
26 |
27 | describe("With no conditions", () => {
28 | const command = "SELECT * FROM table"
29 |
30 | test("It returns undefined", () => {
31 | expect(parseWhereCommand(command)).toBeUndefined()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/custom-database/script.js:
--------------------------------------------------------------------------------
1 | const readline = require("readline")
2 | const parseCommand = require("./parseCommand")
3 | const rl = readline.createInterface({
4 | input: process.stdin,
5 | output: process.stdout,
6 | })
7 |
8 | async function start() {
9 | while (true) {
10 | try {
11 | const commandString = await waitForCommand()
12 | printFormattedJSON(await parseCommand(commandString))
13 | } catch (e) {
14 | console.error(`${e.name}: ${e.message}`)
15 | }
16 | }
17 | }
18 | start()
19 |
20 | function waitForCommand() {
21 | return new Promise(resolve => {
22 | rl.question("> ", resolve)
23 | })
24 | }
25 |
26 | function printFormattedJSON(string) {
27 | console.log(JSON.stringify(string, null, 2))
28 | }
29 |
--------------------------------------------------------------------------------
/custom-database/utils/safeParseJSON.js:
--------------------------------------------------------------------------------
1 | module.exports = function safeParseJSON(string) {
2 | try {
3 | return JSON.parse(string)
4 | } catch (e) {
5 | return
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/ecommerce/after/client/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .parcel-cache
3 | dist
4 | .env
--------------------------------------------------------------------------------
/ecommerce/after/client/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 |
3 | const apiInstance = axios.create({
4 | baseURL: process.env.SERVER_URL,
5 | withCredentials: true,
6 | })
7 | const stripe = Stripe(process.env.STRIPE_PUBLIC_KEY)
8 |
9 | export async function downloadAll(email) {
10 | return apiInstance
11 | .post("/download-all", { email })
12 | .then(res => alert(res.data.message))
13 | .catch(res => alert(res.data.message))
14 | }
15 |
16 | export async function getItems() {
17 | const res = await apiInstance.get("/items")
18 | return res.data
19 | }
20 |
21 | export function downloadItem(itemId) {
22 | return apiInstance
23 | .post("/download-email", { itemId })
24 | .then(res => alert(res.data.message))
25 | .catch(res => alert(res.data.message))
26 | }
27 |
28 | export function purchaseItem(itemId) {
29 | return apiInstance
30 | .post("/create-checkout-session", {
31 | itemId,
32 | })
33 | .then(res => {
34 | return stripe.redirectToCheckout({ sessionId: res.data.id })
35 | })
36 | .then(function (result) {
37 | if (result.error) {
38 | alert(result.error.message)
39 | }
40 | })
41 | .catch(function (error) {
42 | console.error("Error:", error)
43 | alert(error)
44 | })
45 | }
46 |
--------------------------------------------------------------------------------
/ecommerce/after/client/download-links.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Download Links
8 |
9 |
10 | Check your email for the link to download your product
11 |
12 |
--------------------------------------------------------------------------------
/ecommerce/after/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Store
9 |
10 |
11 |
12 |
13 | Store
14 |
21 |
22 |
23 |
24 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/ecommerce/after/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "axios": "^0.21.1"
4 | },
5 | "devDependencies": {
6 | "parcel": "^2.0.0-beta.2"
7 | },
8 | "name": "client",
9 | "version": "1.0.0",
10 | "main": "api.js",
11 | "scripts": {
12 | "start": "parcel index.html download-links.html"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "description": ""
18 | }
19 |
--------------------------------------------------------------------------------
/ecommerce/after/client/script.js:
--------------------------------------------------------------------------------
1 | import { getItems, purchaseItem, downloadItem, downloadAll } from "./api"
2 |
3 | const itemTemplate = document.getElementById("item-template")
4 | const itemList = document.querySelector("[data-item-list]")
5 | const emailForm = document.querySelector("[data-email-form]")
6 | const emailInput = document.querySelector("[data-email-input]")
7 |
8 | emailForm.addEventListener("submit", async e => {
9 | e.preventDefault()
10 | await downloadAll(emailInput.value)
11 | window.location = window.location
12 | })
13 |
14 | async function loadItems() {
15 | const items = await getItems()
16 |
17 | itemList.innerHTML = ""
18 | items.forEach(item => {
19 | const itemElement = itemTemplate.content.cloneNode(true)
20 | itemElement.querySelector("[data-item-name]").textContent = item.name
21 | const priceElement = itemElement.querySelector("[data-item-price]")
22 | priceElement.textContent = `$${item.price}`
23 |
24 | const button = itemElement.querySelector("[data-item-btn]")
25 | if (item.purchased) {
26 | button.classList.add("download-btn")
27 | button.textContent = "Download"
28 | button.addEventListener("click", () => {
29 | downloadItem(item.id)
30 | })
31 | } else {
32 | button.classList.add("purchase-btn")
33 | button.textContent = "Purchase"
34 | button.addEventListener("click", () => {
35 | purchaseItem(item.id)
36 | })
37 | }
38 |
39 | itemList.append(itemElement)
40 | })
41 | }
42 |
43 | loadItems()
44 |
--------------------------------------------------------------------------------
/ecommerce/after/client/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
3 | }
4 |
5 | .title {
6 | text-align: center;
7 | margin-bottom: .5rem;
8 | color: #333;
9 | }
10 |
11 | .sign-in-form {
12 | max-width: 100%;
13 | width: max-content;
14 | margin: auto;
15 | }
16 |
17 | .sign-in-helper-text {
18 | margin-bottom: .25rem;
19 | color: #555;
20 | font-size: .9rem;
21 | }
22 |
23 | .form-input {
24 | display: flex;
25 | width: 100%;
26 | }
27 |
28 | .input {
29 | flex-grow: 1;
30 | border: 1px solid #555;
31 | border-right: none;
32 | }
33 |
34 | .btn.btn-login {
35 | border: 1px solid #555;
36 | background-color: #1891ce;
37 | color: white;
38 | }
39 |
40 | .btn.btn-login:hover {
41 | background-color: #116b97;
42 | }
43 |
44 | .item-list {
45 | display: grid;
46 | justify-content: center;
47 | grid-template-columns: repeat(auto-fit, 180px);
48 | gap: 1rem;
49 | max-width: 90%;
50 | margin: auto;
51 | margin-top: 2rem;
52 | }
53 |
54 | .item {
55 | text-align: center;
56 | border: 1px solid #555;
57 | border-radius: .25rem;
58 | padding: .5rem;
59 | }
60 |
61 | .item-name {
62 | font-weight: bold;
63 | color: #333;
64 | }
65 |
66 | .item-footer {
67 | color: #555;
68 | display: flex;
69 | justify-content: space-between;
70 | margin-top: .5rem;
71 | align-items: center;
72 | }
73 |
74 | .btn {
75 | border: none;
76 | padding: .25rem .5rem;
77 | cursor: pointer;
78 | }
79 |
80 | .purchase-btn {
81 | background-color: #1ba100;
82 | color: white;
83 | }
84 |
85 | .purchase-btn:hover {
86 | background-color: #147500;
87 | }
88 |
89 | .download-btn {
90 | background-color: #8d00c5;
91 | color: white;
92 | }
93 |
94 | .download-btn:hover {
95 | background-color: #6500a0;
96 | }
--------------------------------------------------------------------------------
/ecommerce/after/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
--------------------------------------------------------------------------------
/ecommerce/after/server/contacts.js:
--------------------------------------------------------------------------------
1 | const apiInstance = require("./sendInBlueApiInstance")
2 | const items = require("./items.json")
3 |
4 | async function linkContactAndItem(email, { listId }) {
5 | const contact = await getContact(email)
6 | if (contact == null) {
7 | return createContact(email, listId)
8 | } else {
9 | return updateContact(contact.id, listId)
10 | }
11 | }
12 |
13 | async function getContactPurchasedItems(email) {
14 | if (email == null) return []
15 | const contact = await getContact(email)
16 | if (contact == null) return []
17 | return items.filter(item => contact.listIds.includes(item.listId))
18 | }
19 |
20 | function createContact(email, listId) {
21 | return apiInstance.post("/contacts", {
22 | email,
23 | listIds: [listId],
24 | })
25 | }
26 |
27 | function updateContact(emailOrId, listId) {
28 | return apiInstance.put(`/contacts/${emailOrId}`, {
29 | listIds: [listId],
30 | })
31 | }
32 |
33 | function getContact(emailOrId) {
34 | return apiInstance
35 | .get(`/contacts/${emailOrId}`)
36 | .then(res => res.data)
37 | .catch(e => {
38 | if (e.response.status === 404) return null
39 | throw e
40 | })
41 | }
42 |
43 | module.exports = { linkContactAndItem, getContactPurchasedItems }
44 |
--------------------------------------------------------------------------------
/ecommerce/after/server/downloads/javascript-simplified.txt:
--------------------------------------------------------------------------------
1 | JavaScript Simplified Course
--------------------------------------------------------------------------------
/ecommerce/after/server/downloads/learn-css-today.txt:
--------------------------------------------------------------------------------
1 | Learn CSS Today Course
--------------------------------------------------------------------------------
/ecommerce/after/server/downloads/learn-react-today.txt:
--------------------------------------------------------------------------------
1 | Learn React Today Course
--------------------------------------------------------------------------------
/ecommerce/after/server/items.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "Learn React Today",
5 | "priceInCents": 10000,
6 | "file": "learn-react-today.txt",
7 | "listId": 5
8 | },
9 | {
10 | "id": 2,
11 | "name": "Learn CSS Today",
12 | "priceInCents": 20000,
13 | "file": "learn-css-today.txt",
14 | "listId": 6
15 | },
16 | {
17 | "id": 3,
18 | "name": "JavaScript Simplified",
19 | "priceInCents": 30000,
20 | "file": "javascript-simplified.txt",
21 | "listId": 7
22 | }
23 | ]
--------------------------------------------------------------------------------
/ecommerce/after/server/mailer.js:
--------------------------------------------------------------------------------
1 | const apiInstance = require("./sendInBlueApiInstance")
2 |
3 | function sendDownloadLink(email, downloadLinkCode, item) {
4 | const downloadLink = `${process.env.SERVER_URL}/download/${downloadLinkCode}`
5 |
6 | return sendEmail({
7 | email,
8 | subject: `Download ${item.name}`,
9 | htmlContent: `
10 | Thank you for purchasing ${item.name}
11 |
12 | Download it now
13 | `,
14 | textContent: `Thank you for purchasing ${item.name}
15 | Download it now. ${downloadLink}`,
16 | })
17 | }
18 |
19 | function sendAllDownloadLinks(email, downloadableItems) {
20 | if (downloadableItems.length === 0) return
21 |
22 | return sendEmail({
23 | email,
24 | subject: "Download Your Files",
25 | htmlContent: downloadableItems
26 | .map(({ item, code }) => {
27 | return `Download ${item.name}`
28 | })
29 | .join("
"),
30 | textContent: downloadableItems
31 | .map(({ item, code }) => {
32 | return `Download ${item.name} ${process.env.SERVER_URL}/download/${code}`
33 | })
34 | .join("\n"),
35 | })
36 | }
37 |
38 | function sendEmail({ email, ...options }) {
39 | const sender = {
40 | name: "Kyle From Web Dev Simplified",
41 | email: "kyle@webdevsimplified.com",
42 | }
43 |
44 | return apiInstance.post("/smtp/email", {
45 | sender,
46 | replyTo: sender,
47 | to: [{ email }],
48 | ...options,
49 | })
50 | }
51 |
52 | module.exports = { sendDownloadLink, sendAllDownloadLinks }
53 |
--------------------------------------------------------------------------------
/ecommerce/after/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "devStart": "nodemon server.js",
8 | "start": "node server.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "axios": "^0.21.1",
15 | "cookie-parser": "^1.4.5",
16 | "cors": "^2.8.5",
17 | "dotenv": "^8.2.0",
18 | "express": "^4.17.1",
19 | "stripe": "^8.144.0",
20 | "uuid": "^8.3.2"
21 | },
22 | "devDependencies": {
23 | "nodemon": "^2.0.7"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ecommerce/after/server/sendInBlueApiInstance.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios")
2 |
3 | module.exports = axios.create({
4 | baseURL: "https://api.sendinblue.com/v3",
5 | headers: { "api-key": process.env.SEND_IN_BLUE_API_KEY },
6 | })
7 |
--------------------------------------------------------------------------------
/ecommerce/after/server/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config()
2 | const express = require("express")
3 | const cookieParser = require("cookie-parser")
4 | const app = express()
5 | const cors = require("cors")
6 | const items = require("./items.json")
7 | const stripe = require("stripe")(process.env.STRIPE_PRIVATE_KEY)
8 | const { v4: uuidV4 } = require("uuid")
9 | const { sendDownloadLink, sendAllDownloadLinks } = require("./mailer")
10 | const { linkContactAndItem, getContactPurchasedItems } = require("./contacts")
11 |
12 | const downloadLinkMap = new Map()
13 | const DOWNLOAD_LINK_EXPIRATION = 10 * 60 * 1000 // 10 Minutes
14 | const COOKIE_EXPIRATION = 30 * 24 * 60 * 60 * 1000 // 30 Days
15 |
16 | app.use(cookieParser())
17 | app.use(express.json())
18 | app.use(express.urlencoded({ extended: true }))
19 | app.use(
20 | cors({
21 | credentials: true,
22 | origin: process.env.CLIENT_URL,
23 | })
24 | )
25 |
26 | app.get("/items", async (req, res) => {
27 | const email = req.cookies.email
28 | const purchasedItemIds = (await getContactPurchasedItems(email)).map(
29 | item => item.id
30 | )
31 | res.json(
32 | items.map(item => {
33 | return {
34 | id: item.id,
35 | name: item.name,
36 | price: item.priceInCents / 100,
37 | purchased: purchasedItemIds.includes(item.id),
38 | }
39 | })
40 | )
41 | })
42 |
43 | app.post("/download-email", (req, res) => {
44 | const email = req.cookies.email
45 | const itemId = req.body.itemId
46 | const code = createDownloadCode(itemId)
47 | sendDownloadLink(
48 | email,
49 | code,
50 | items.find(i => i.id === parseInt(itemId))
51 | )
52 | .then(() => {
53 | res.json({ message: "Check your email" })
54 | })
55 | .catch(() => {
56 | res.status(500).json({ message: "Error: Please try again" })
57 | })
58 | })
59 |
60 | app.post("/download-all", async (req, res) => {
61 | const email = req.body.email
62 | const items = await getContactPurchasedItems(email)
63 | setEmailCookie(res, email)
64 | sendAllDownloadLinks(
65 | email,
66 | items.map(item => {
67 | return { item, code: createDownloadCode(item.id) }
68 | })
69 | )
70 |
71 | return res.json({ message: "Check your email for a download link" })
72 | })
73 |
74 | app.post("/create-checkout-session", async (req, res) => {
75 | const item = items.find(i => i.id === parseInt(req.body.itemId))
76 | if (item == null) {
77 | return res.status(400).json({ message: "Invalid Item" })
78 | }
79 | const session = await createCheckoutSession(item)
80 | res.json({ id: session.id })
81 | })
82 |
83 | app.get("/download/:code", (req, res) => {
84 | const itemId = downloadLinkMap.get(req.params.code)
85 | if (itemId == null) {
86 | return res.send("This link has either expired or is invalid")
87 | }
88 |
89 | const item = items.find(i => i.id === itemId)
90 | if (item == null) {
91 | return res.send("This item could not be found")
92 | }
93 |
94 | downloadLinkMap.delete(req.params.code)
95 | res.download(`downloads/${item.file}`)
96 | })
97 |
98 | app.get("/purchase-success", async (req, res) => {
99 | const item = items.find(i => i.id === parseInt(req.query.itemId))
100 | const {
101 | customer_details: { email },
102 | } = await stripe.checkout.sessions.retrieve(req.query.sessionId)
103 |
104 | setEmailCookie(res, email)
105 | linkContactAndItem(email, item)
106 | const downloadLinkCode = createDownloadCode(item.id)
107 | sendDownloadLink(email, downloadLinkCode, item)
108 | res.redirect(`${process.env.CLIENT_URL}/download-links.html`)
109 | })
110 |
111 | function setEmailCookie(res, email) {
112 | res.cookie("email", email, {
113 | httpOnly: true,
114 | secure: true,
115 | maxAge: COOKIE_EXPIRATION,
116 | sameSite: "None",
117 | })
118 | }
119 |
120 | function createCheckoutSession(item) {
121 | return stripe.checkout.sessions.create({
122 | payment_method_types: ["card"],
123 | line_items: [
124 | {
125 | price_data: {
126 | currency: "usd",
127 | product_data: {
128 | name: item.name,
129 | },
130 | unit_amount: item.priceInCents,
131 | },
132 | quantity: 1,
133 | },
134 | ],
135 | mode: "payment",
136 | success_url: `${process.env.SERVER_URL}/purchase-success?itemId=${item.id}&sessionId={CHECKOUT_SESSION_ID}`,
137 | cancel_url: process.env.CLIENT_URL,
138 | })
139 | }
140 |
141 | function createDownloadCode(itemId) {
142 | const downloadUuid = uuidV4()
143 | downloadLinkMap.set(downloadUuid, itemId)
144 | setTimeout(() => {
145 | downloadLinkMap.delete(downloadUuid)
146 | }, DOWNLOAD_LINK_EXPIRATION)
147 | return downloadUuid
148 | }
149 |
150 | app.listen(3000)
151 |
--------------------------------------------------------------------------------
/ecommerce/before/download-links.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Download Links
8 |
9 |
10 | Check your email for the link to download your product
11 |
12 |
--------------------------------------------------------------------------------
/ecommerce/before/downloads/javascript-simplified.txt:
--------------------------------------------------------------------------------
1 | JavaScript Simplified Course
--------------------------------------------------------------------------------
/ecommerce/before/downloads/learn-css-today.txt:
--------------------------------------------------------------------------------
1 | Learn CSS Today Course
--------------------------------------------------------------------------------
/ecommerce/before/downloads/learn-react-today.txt:
--------------------------------------------------------------------------------
1 | Learn React Today Course
--------------------------------------------------------------------------------
/ecommerce/before/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Store
9 |
10 |
11 | Store
12 |
19 |
20 |
21 |
Learn React Today
22 |
26 |
27 |
28 |
Learn CSS Today
29 |
33 |
34 |
35 |
JavaScript Simplified
36 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/ecommerce/before/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
3 | }
4 |
5 | .title {
6 | text-align: center;
7 | margin-bottom: .5rem;
8 | color: #333;
9 | }
10 |
11 | .sign-in-form {
12 | max-width: 100%;
13 | width: max-content;
14 | margin: auto;
15 | }
16 |
17 | .sign-in-helper-text {
18 | margin-bottom: .25rem;
19 | color: #555;
20 | font-size: .9rem;
21 | }
22 |
23 | .form-input {
24 | display: flex;
25 | width: 100%;
26 | }
27 |
28 | .input {
29 | flex-grow: 1;
30 | border: 1px solid #555;
31 | border-right: none;
32 | }
33 |
34 | .btn.btn-login {
35 | border: 1px solid #555;
36 | background-color: #1891ce;
37 | color: white;
38 | }
39 |
40 | .btn.btn-login:hover {
41 | background-color: #116b97;
42 | }
43 |
44 | .item-list {
45 | display: grid;
46 | justify-content: center;
47 | grid-template-columns: repeat(auto-fit, 180px);
48 | gap: 1rem;
49 | max-width: 90%;
50 | margin: auto;
51 | margin-top: 2rem;
52 | }
53 |
54 | .item {
55 | text-align: center;
56 | border: 1px solid #555;
57 | border-radius: .25rem;
58 | padding: .5rem;
59 | }
60 |
61 | .item-name {
62 | font-weight: bold;
63 | color: #333;
64 | }
65 |
66 | .item-footer {
67 | color: #555;
68 | display: flex;
69 | justify-content: space-between;
70 | margin-top: .5rem;
71 | align-items: center;
72 | }
73 |
74 | .btn {
75 | border: none;
76 | padding: .25rem .5rem;
77 | cursor: pointer;
78 | }
79 |
80 | .purchase-btn {
81 | background-color: #1ba100;
82 | color: white;
83 | }
84 |
85 | .purchase-btn:hover {
86 | background-color: #147500;
87 | }
88 |
89 | .download-btn {
90 | background-color: #8d00c5;
91 | color: white;
92 | }
93 |
94 | .download-btn:hover {
95 | background-color: #6500a0;
96 | }
--------------------------------------------------------------------------------
/pictionary-clone/after/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/pictionary-clone/after/client/DrawableCanvas.js:
--------------------------------------------------------------------------------
1 | export default function DrawableCanvas(canvas, socket) {
2 | this.canDraw = false
3 | this.clearCanvas = function() {
4 | const ctx = canvas.getContext("2d")
5 | ctx.clearRect(0, 0, canvas.width, canvas.height)
6 | }
7 |
8 | let prevPosition = null
9 |
10 | canvas.addEventListener("mousemove", e => {
11 | if (e.buttons !== 1 || !this.canDraw) {
12 | prevPosition = null
13 | return
14 | }
15 |
16 | const newPosition = { x: e.layerX, y: e.layerY }
17 | if (prevPosition != null) {
18 | drawLine(prevPosition, newPosition)
19 | socket.emit("draw", {
20 | start: normalizeCoordinates(prevPosition),
21 | end: normalizeCoordinates(newPosition)
22 | })
23 | }
24 |
25 | prevPosition = newPosition
26 | })
27 | canvas.addEventListener("mouseleave", () => (prevPosition = null))
28 | socket.on("draw-line", (start, end) => {
29 | drawLine(toCanvasSpace(start), toCanvasSpace(end))
30 | })
31 |
32 | function drawLine(start, end) {
33 | const ctx = canvas.getContext("2d")
34 | ctx.beginPath()
35 | ctx.moveTo(start.x, start.y)
36 | ctx.lineTo(end.x, end.y)
37 | ctx.stroke()
38 | }
39 |
40 | function normalizeCoordinates(position) {
41 | return {
42 | x: position.x / canvas.width,
43 | y: position.y / canvas.height
44 | }
45 | }
46 |
47 | function toCanvasSpace(position) {
48 | return {
49 | x: position.x * canvas.width,
50 | y: position.y * canvas.height
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/pictionary-clone/after/client/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | min-height: 100vh;
10 | margin: 0;
11 | }
12 |
13 | .submit-btn {
14 | width: 100%;
15 | color: white;
16 | background-color: #333;
17 | border: none;
18 | cursor: pointer;
19 | padding: .5rem 1rem;
20 | }
21 |
22 | .submit-btn:hover {
23 | background-color: #555;
24 | }
25 |
26 | form {
27 | max-width: 300px;
28 | width: 100%;
29 | }
30 |
31 | .form-group {
32 | display: flex;
33 | flex-direction: column;
34 | margin-bottom: .75rem;
35 | }
36 |
37 | .form-group > label {
38 | font-weight: bold;
39 | font-size: .8rem;
40 | color: #555;
41 | }
42 |
43 | .form-group > input {
44 | font-size: 1rem;
45 | }
--------------------------------------------------------------------------------
/pictionary-clone/after/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pictionary
8 |
9 |
10 |
11 |
22 |
23 |
--------------------------------------------------------------------------------
/pictionary-clone/after/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel index.html room.html",
8 | "build": "parcel build index.html room.html"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "parcel-bundler": "^1.12.4"
15 | },
16 | "dependencies": {
17 | "socket.io-client": "^3.0.4"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pictionary-clone/after/client/room.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | padding: .5rem;
7 | margin: 0;
8 | height: 100vh;
9 | display: flex;
10 | background-color: #333;
11 | }
12 |
13 | .game-board {
14 | height: 100%;
15 | width: 100%;
16 | background-color: white;
17 | display: block;
18 | }
19 |
20 | .text-section {
21 | margin-left: .5rem;
22 | display: flex;
23 | flex-direction: column;
24 | flex-shrink: 0;
25 | flex-grow: 0;
26 | width: 170px;
27 | }
28 |
29 | .word {
30 | margin: 0;
31 | margin-bottom: .25rem;
32 | color: white;
33 | text-align: center;
34 | font-weight: normal;
35 | }
36 |
37 | .messages {
38 | flex-grow: 1;
39 | min-height: 5rem;
40 | display: flex;
41 | flex-direction: column;
42 | justify-content: flex-end;
43 | overflow: hidden;
44 | }
45 |
46 | .guess-form {
47 | display: flex;
48 | flex-direction: column;
49 | }
50 |
51 | .guess-input {
52 | margin: .25rem 0;
53 | }
54 |
55 | .guess-btn {
56 | cursor: pointer;
57 | }
58 |
59 | .ready-btn {
60 | cursor: pointer;
61 | }
62 |
63 | .guess-container {
64 | background-color: #CCC;
65 | border-radius: .25rem;
66 | padding: .4rem;
67 | width: 100%;
68 | margin-bottom: .25rem;
69 | }
70 |
71 | .guess-container > .name {
72 | font-weight: bold;
73 | font-size: .75rem;
74 | color: #555;
75 | word-break: break-all;
76 | }
77 |
78 | .guess-container > .text {
79 | word-break: break-all;
80 | }
81 |
82 | @media (max-width: 500px) {
83 | body {
84 | flex-direction: column;
85 | }
86 |
87 | .text-section {
88 | margin-left: 0;
89 | margin-top: .5rem;
90 | width: 100%;
91 | }
92 | }
93 |
94 | .hide {
95 | display: none !important;
96 | }
--------------------------------------------------------------------------------
/pictionary-clone/after/client/room.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pictionary
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/pictionary-clone/after/client/room.js:
--------------------------------------------------------------------------------
1 | import { io } from "socket.io-client"
2 | import DrawableCanvas from "./DrawableCanvas"
3 |
4 | const production = process.env.NODE_ENV === "production"
5 | const serverURL = production ? "realsite.com" : "http://localhost:3000"
6 |
7 | const urlParams = new URLSearchParams(window.location.search)
8 | const name = urlParams.get("name")
9 | const roomId = urlParams.get("room-id")
10 |
11 | if (!name || !roomId) window.location = "/index.html"
12 |
13 | const socket = io(serverURL)
14 | const guessForm = document.querySelector("[data-guess-form]")
15 | const guessInput = document.querySelector("[data-guess-input]")
16 | const wordElement = document.querySelector("[data-word]")
17 | const messagesElement = document.querySelector("[data-messages]")
18 | const readyButton = document.querySelector("[data-ready-btn]")
19 | const canvas = document.querySelector("[data-canvas]")
20 | const drawableCanvas = new DrawableCanvas(canvas, socket)
21 | const guessTemplate = document.querySelector("[data-guess-template]")
22 |
23 | socket.emit("join-room", { name: name, roomId: roomId })
24 | socket.on("start-drawer", startRoundDrawer)
25 | socket.on("start-guesser", startRoundGuesser)
26 | socket.on("guess", displayGuess)
27 | socket.on("winner", endRound)
28 | endRound()
29 | resizeCanvas()
30 | setupHTMLEvents()
31 |
32 | function setupHTMLEvents() {
33 | readyButton.addEventListener("click", () => {
34 | hide(readyButton)
35 | socket.emit("ready")
36 | })
37 |
38 | guessForm.addEventListener("submit", e => {
39 | e.preventDefault()
40 |
41 | if (guessInput.value === "") return
42 |
43 | socket.emit("make-guess", { guess: guessInput.value })
44 | displayGuess(name, guessInput.value)
45 |
46 | guessInput.value = ""
47 | })
48 |
49 | window.addEventListener("resize", resizeCanvas)
50 | }
51 |
52 | function displayGuess(guesserName, guess) {
53 | const guessElement = guessTemplate.content.cloneNode(true)
54 | const messageElement = guessElement.querySelector("[data-text]")
55 | const nameElement = guessElement.querySelector("[data-name]")
56 | nameElement.innerText = guesserName
57 | messageElement.innerText = guess
58 | messagesElement.append(guessElement)
59 | }
60 |
61 | function startRoundDrawer(word) {
62 | drawableCanvas.canDraw = true
63 | drawableCanvas.clearCanvas()
64 |
65 | messagesElement.innerHTML = ""
66 | wordElement.innerText = word
67 | }
68 |
69 | function startRoundGuesser() {
70 | show(guessForm)
71 | hide(wordElement)
72 | drawableCanvas.clearCanvas()
73 |
74 | messagesElement.innerHTML = ""
75 | wordElement.innerText = ""
76 | }
77 |
78 | function resizeCanvas() {
79 | canvas.width = null
80 | canvas.height = null
81 | const clientDimensions = canvas.getBoundingClientRect()
82 | canvas.width = clientDimensions.width
83 | canvas.height = clientDimensions.height
84 | }
85 |
86 | function endRound(name, word) {
87 | if (word && name) {
88 | wordElement.innerText = word
89 | show(wordElement)
90 | displayGuess(null, `${name} is the winner`)
91 | }
92 |
93 | drawableCanvas.canDraw = false
94 | show(readyButton)
95 | hide(guessForm)
96 | }
97 |
98 | function hide(element) {
99 | element.classList.add("hide")
100 | }
101 |
102 | function show(element) {
103 | element.classList.remove("hide")
104 | }
105 |
--------------------------------------------------------------------------------
/pictionary-clone/after/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@sindresorhus/is": {
8 | "version": "0.14.0",
9 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
10 | "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
11 | "dev": true
12 | },
13 | "@szmarczak/http-timer": {
14 | "version": "1.1.2",
15 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
16 | "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
17 | "dev": true,
18 | "requires": {
19 | "defer-to-connect": "^1.0.1"
20 | }
21 | },
22 | "@types/component-emitter": {
23 | "version": "1.2.10",
24 | "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
25 | "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
26 | },
27 | "@types/cookie": {
28 | "version": "0.4.0",
29 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
30 | "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
31 | },
32 | "@types/cors": {
33 | "version": "2.8.9",
34 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz",
35 | "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg=="
36 | },
37 | "@types/node": {
38 | "version": "14.14.12",
39 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.12.tgz",
40 | "integrity": "sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g=="
41 | },
42 | "abbrev": {
43 | "version": "1.1.1",
44 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
45 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
46 | "dev": true
47 | },
48 | "accepts": {
49 | "version": "1.3.7",
50 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
51 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
52 | "requires": {
53 | "mime-types": "~2.1.24",
54 | "negotiator": "0.6.2"
55 | }
56 | },
57 | "ansi-align": {
58 | "version": "3.0.0",
59 | "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz",
60 | "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==",
61 | "dev": true,
62 | "requires": {
63 | "string-width": "^3.0.0"
64 | },
65 | "dependencies": {
66 | "string-width": {
67 | "version": "3.1.0",
68 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
69 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
70 | "dev": true,
71 | "requires": {
72 | "emoji-regex": "^7.0.1",
73 | "is-fullwidth-code-point": "^2.0.0",
74 | "strip-ansi": "^5.1.0"
75 | }
76 | }
77 | }
78 | },
79 | "ansi-regex": {
80 | "version": "4.1.0",
81 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
82 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
83 | "dev": true
84 | },
85 | "ansi-styles": {
86 | "version": "4.3.0",
87 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
88 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
89 | "dev": true,
90 | "requires": {
91 | "color-convert": "^2.0.1"
92 | }
93 | },
94 | "anymatch": {
95 | "version": "3.1.1",
96 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
97 | "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
98 | "dev": true,
99 | "requires": {
100 | "normalize-path": "^3.0.0",
101 | "picomatch": "^2.0.4"
102 | }
103 | },
104 | "balanced-match": {
105 | "version": "1.0.0",
106 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
107 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
108 | "dev": true
109 | },
110 | "base64-arraybuffer": {
111 | "version": "0.1.4",
112 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
113 | "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
114 | },
115 | "base64id": {
116 | "version": "2.0.0",
117 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
118 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
119 | },
120 | "binary-extensions": {
121 | "version": "2.1.0",
122 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
123 | "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
124 | "dev": true
125 | },
126 | "boxen": {
127 | "version": "4.2.0",
128 | "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz",
129 | "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==",
130 | "dev": true,
131 | "requires": {
132 | "ansi-align": "^3.0.0",
133 | "camelcase": "^5.3.1",
134 | "chalk": "^3.0.0",
135 | "cli-boxes": "^2.2.0",
136 | "string-width": "^4.1.0",
137 | "term-size": "^2.1.0",
138 | "type-fest": "^0.8.1",
139 | "widest-line": "^3.1.0"
140 | }
141 | },
142 | "brace-expansion": {
143 | "version": "1.1.11",
144 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
145 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
146 | "dev": true,
147 | "requires": {
148 | "balanced-match": "^1.0.0",
149 | "concat-map": "0.0.1"
150 | }
151 | },
152 | "braces": {
153 | "version": "3.0.2",
154 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
155 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
156 | "dev": true,
157 | "requires": {
158 | "fill-range": "^7.0.1"
159 | }
160 | },
161 | "cacheable-request": {
162 | "version": "6.1.0",
163 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
164 | "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
165 | "dev": true,
166 | "requires": {
167 | "clone-response": "^1.0.2",
168 | "get-stream": "^5.1.0",
169 | "http-cache-semantics": "^4.0.0",
170 | "keyv": "^3.0.0",
171 | "lowercase-keys": "^2.0.0",
172 | "normalize-url": "^4.1.0",
173 | "responselike": "^1.0.2"
174 | },
175 | "dependencies": {
176 | "get-stream": {
177 | "version": "5.2.0",
178 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
179 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
180 | "dev": true,
181 | "requires": {
182 | "pump": "^3.0.0"
183 | }
184 | },
185 | "lowercase-keys": {
186 | "version": "2.0.0",
187 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
188 | "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
189 | "dev": true
190 | }
191 | }
192 | },
193 | "camelcase": {
194 | "version": "5.3.1",
195 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
196 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
197 | "dev": true
198 | },
199 | "chalk": {
200 | "version": "3.0.0",
201 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
202 | "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
203 | "dev": true,
204 | "requires": {
205 | "ansi-styles": "^4.1.0",
206 | "supports-color": "^7.1.0"
207 | },
208 | "dependencies": {
209 | "has-flag": {
210 | "version": "4.0.0",
211 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
212 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
213 | "dev": true
214 | },
215 | "supports-color": {
216 | "version": "7.2.0",
217 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
218 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
219 | "dev": true,
220 | "requires": {
221 | "has-flag": "^4.0.0"
222 | }
223 | }
224 | }
225 | },
226 | "chokidar": {
227 | "version": "3.4.3",
228 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz",
229 | "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==",
230 | "dev": true,
231 | "requires": {
232 | "anymatch": "~3.1.1",
233 | "braces": "~3.0.2",
234 | "fsevents": "~2.1.2",
235 | "glob-parent": "~5.1.0",
236 | "is-binary-path": "~2.1.0",
237 | "is-glob": "~4.0.1",
238 | "normalize-path": "~3.0.0",
239 | "readdirp": "~3.5.0"
240 | }
241 | },
242 | "ci-info": {
243 | "version": "2.0.0",
244 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
245 | "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
246 | "dev": true
247 | },
248 | "cli-boxes": {
249 | "version": "2.2.1",
250 | "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
251 | "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
252 | "dev": true
253 | },
254 | "clone-response": {
255 | "version": "1.0.2",
256 | "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
257 | "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
258 | "dev": true,
259 | "requires": {
260 | "mimic-response": "^1.0.0"
261 | }
262 | },
263 | "color-convert": {
264 | "version": "2.0.1",
265 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
266 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
267 | "dev": true,
268 | "requires": {
269 | "color-name": "~1.1.4"
270 | }
271 | },
272 | "color-name": {
273 | "version": "1.1.4",
274 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
275 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
276 | "dev": true
277 | },
278 | "component-emitter": {
279 | "version": "1.3.0",
280 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
281 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
282 | },
283 | "concat-map": {
284 | "version": "0.0.1",
285 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
286 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
287 | "dev": true
288 | },
289 | "configstore": {
290 | "version": "5.0.1",
291 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
292 | "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
293 | "dev": true,
294 | "requires": {
295 | "dot-prop": "^5.2.0",
296 | "graceful-fs": "^4.1.2",
297 | "make-dir": "^3.0.0",
298 | "unique-string": "^2.0.0",
299 | "write-file-atomic": "^3.0.0",
300 | "xdg-basedir": "^4.0.0"
301 | }
302 | },
303 | "cookie": {
304 | "version": "0.4.1",
305 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
306 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
307 | },
308 | "cors": {
309 | "version": "2.8.5",
310 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
311 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
312 | "requires": {
313 | "object-assign": "^4",
314 | "vary": "^1"
315 | }
316 | },
317 | "crypto-random-string": {
318 | "version": "2.0.0",
319 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
320 | "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
321 | "dev": true
322 | },
323 | "debug": {
324 | "version": "4.1.1",
325 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
326 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
327 | "requires": {
328 | "ms": "^2.1.1"
329 | }
330 | },
331 | "decompress-response": {
332 | "version": "3.3.0",
333 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
334 | "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
335 | "dev": true,
336 | "requires": {
337 | "mimic-response": "^1.0.0"
338 | }
339 | },
340 | "deep-extend": {
341 | "version": "0.6.0",
342 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
343 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
344 | "dev": true
345 | },
346 | "defer-to-connect": {
347 | "version": "1.1.3",
348 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
349 | "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
350 | "dev": true
351 | },
352 | "dot-prop": {
353 | "version": "5.3.0",
354 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
355 | "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
356 | "dev": true,
357 | "requires": {
358 | "is-obj": "^2.0.0"
359 | }
360 | },
361 | "duplexer3": {
362 | "version": "0.1.4",
363 | "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
364 | "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
365 | "dev": true
366 | },
367 | "emoji-regex": {
368 | "version": "7.0.3",
369 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
370 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
371 | "dev": true
372 | },
373 | "end-of-stream": {
374 | "version": "1.4.4",
375 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
376 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
377 | "dev": true,
378 | "requires": {
379 | "once": "^1.4.0"
380 | }
381 | },
382 | "engine.io": {
383 | "version": "4.0.5",
384 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.5.tgz",
385 | "integrity": "sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==",
386 | "requires": {
387 | "accepts": "~1.3.4",
388 | "base64id": "2.0.0",
389 | "cookie": "~0.4.1",
390 | "cors": "~2.8.5",
391 | "debug": "~4.1.0",
392 | "engine.io-parser": "~4.0.0",
393 | "ws": "^7.1.2"
394 | }
395 | },
396 | "engine.io-parser": {
397 | "version": "4.0.2",
398 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
399 | "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
400 | "requires": {
401 | "base64-arraybuffer": "0.1.4"
402 | }
403 | },
404 | "escape-goat": {
405 | "version": "2.1.1",
406 | "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
407 | "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
408 | "dev": true
409 | },
410 | "fill-range": {
411 | "version": "7.0.1",
412 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
413 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
414 | "dev": true,
415 | "requires": {
416 | "to-regex-range": "^5.0.1"
417 | }
418 | },
419 | "fsevents": {
420 | "version": "2.1.3",
421 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
422 | "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
423 | "dev": true,
424 | "optional": true
425 | },
426 | "get-stream": {
427 | "version": "4.1.0",
428 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
429 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
430 | "dev": true,
431 | "requires": {
432 | "pump": "^3.0.0"
433 | }
434 | },
435 | "glob-parent": {
436 | "version": "5.1.1",
437 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
438 | "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
439 | "dev": true,
440 | "requires": {
441 | "is-glob": "^4.0.1"
442 | }
443 | },
444 | "global-dirs": {
445 | "version": "2.0.1",
446 | "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz",
447 | "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==",
448 | "dev": true,
449 | "requires": {
450 | "ini": "^1.3.5"
451 | }
452 | },
453 | "got": {
454 | "version": "9.6.0",
455 | "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
456 | "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
457 | "dev": true,
458 | "requires": {
459 | "@sindresorhus/is": "^0.14.0",
460 | "@szmarczak/http-timer": "^1.1.2",
461 | "cacheable-request": "^6.0.0",
462 | "decompress-response": "^3.3.0",
463 | "duplexer3": "^0.1.4",
464 | "get-stream": "^4.1.0",
465 | "lowercase-keys": "^1.0.1",
466 | "mimic-response": "^1.0.1",
467 | "p-cancelable": "^1.0.0",
468 | "to-readable-stream": "^1.0.0",
469 | "url-parse-lax": "^3.0.0"
470 | }
471 | },
472 | "graceful-fs": {
473 | "version": "4.2.4",
474 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
475 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
476 | "dev": true
477 | },
478 | "has-flag": {
479 | "version": "3.0.0",
480 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
481 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
482 | "dev": true
483 | },
484 | "has-yarn": {
485 | "version": "2.1.0",
486 | "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
487 | "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
488 | "dev": true
489 | },
490 | "http-cache-semantics": {
491 | "version": "4.1.0",
492 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
493 | "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
494 | "dev": true
495 | },
496 | "ignore-by-default": {
497 | "version": "1.0.1",
498 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
499 | "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=",
500 | "dev": true
501 | },
502 | "import-lazy": {
503 | "version": "2.1.0",
504 | "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
505 | "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
506 | "dev": true
507 | },
508 | "imurmurhash": {
509 | "version": "0.1.4",
510 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
511 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
512 | "dev": true
513 | },
514 | "ini": {
515 | "version": "1.3.7",
516 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
517 | "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==",
518 | "dev": true
519 | },
520 | "is-binary-path": {
521 | "version": "2.1.0",
522 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
523 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
524 | "dev": true,
525 | "requires": {
526 | "binary-extensions": "^2.0.0"
527 | }
528 | },
529 | "is-ci": {
530 | "version": "2.0.0",
531 | "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
532 | "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
533 | "dev": true,
534 | "requires": {
535 | "ci-info": "^2.0.0"
536 | }
537 | },
538 | "is-extglob": {
539 | "version": "2.1.1",
540 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
541 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
542 | "dev": true
543 | },
544 | "is-fullwidth-code-point": {
545 | "version": "2.0.0",
546 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
547 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
548 | "dev": true
549 | },
550 | "is-glob": {
551 | "version": "4.0.1",
552 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
553 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
554 | "dev": true,
555 | "requires": {
556 | "is-extglob": "^2.1.1"
557 | }
558 | },
559 | "is-installed-globally": {
560 | "version": "0.3.2",
561 | "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz",
562 | "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==",
563 | "dev": true,
564 | "requires": {
565 | "global-dirs": "^2.0.1",
566 | "is-path-inside": "^3.0.1"
567 | }
568 | },
569 | "is-npm": {
570 | "version": "4.0.0",
571 | "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz",
572 | "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==",
573 | "dev": true
574 | },
575 | "is-number": {
576 | "version": "7.0.0",
577 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
578 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
579 | "dev": true
580 | },
581 | "is-obj": {
582 | "version": "2.0.0",
583 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
584 | "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
585 | "dev": true
586 | },
587 | "is-path-inside": {
588 | "version": "3.0.2",
589 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
590 | "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
591 | "dev": true
592 | },
593 | "is-typedarray": {
594 | "version": "1.0.0",
595 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
596 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
597 | "dev": true
598 | },
599 | "is-yarn-global": {
600 | "version": "0.3.0",
601 | "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
602 | "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
603 | "dev": true
604 | },
605 | "json-buffer": {
606 | "version": "3.0.0",
607 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
608 | "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
609 | "dev": true
610 | },
611 | "keyv": {
612 | "version": "3.1.0",
613 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
614 | "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
615 | "dev": true,
616 | "requires": {
617 | "json-buffer": "3.0.0"
618 | }
619 | },
620 | "latest-version": {
621 | "version": "5.1.0",
622 | "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
623 | "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
624 | "dev": true,
625 | "requires": {
626 | "package-json": "^6.3.0"
627 | }
628 | },
629 | "lowercase-keys": {
630 | "version": "1.0.1",
631 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
632 | "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
633 | "dev": true
634 | },
635 | "make-dir": {
636 | "version": "3.1.0",
637 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
638 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
639 | "dev": true,
640 | "requires": {
641 | "semver": "^6.0.0"
642 | },
643 | "dependencies": {
644 | "semver": {
645 | "version": "6.3.0",
646 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
647 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
648 | "dev": true
649 | }
650 | }
651 | },
652 | "mime-db": {
653 | "version": "1.44.0",
654 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
655 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
656 | },
657 | "mime-types": {
658 | "version": "2.1.27",
659 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
660 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
661 | "requires": {
662 | "mime-db": "1.44.0"
663 | }
664 | },
665 | "mimic-response": {
666 | "version": "1.0.1",
667 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
668 | "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
669 | "dev": true
670 | },
671 | "minimatch": {
672 | "version": "3.0.4",
673 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
674 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
675 | "dev": true,
676 | "requires": {
677 | "brace-expansion": "^1.1.7"
678 | }
679 | },
680 | "minimist": {
681 | "version": "1.2.5",
682 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
683 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
684 | "dev": true
685 | },
686 | "ms": {
687 | "version": "2.1.3",
688 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
689 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
690 | },
691 | "negotiator": {
692 | "version": "0.6.2",
693 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
694 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
695 | },
696 | "nodemon": {
697 | "version": "2.0.6",
698 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz",
699 | "integrity": "sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ==",
700 | "dev": true,
701 | "requires": {
702 | "chokidar": "^3.2.2",
703 | "debug": "^3.2.6",
704 | "ignore-by-default": "^1.0.1",
705 | "minimatch": "^3.0.4",
706 | "pstree.remy": "^1.1.7",
707 | "semver": "^5.7.1",
708 | "supports-color": "^5.5.0",
709 | "touch": "^3.1.0",
710 | "undefsafe": "^2.0.3",
711 | "update-notifier": "^4.1.0"
712 | },
713 | "dependencies": {
714 | "debug": {
715 | "version": "3.2.7",
716 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
717 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
718 | "dev": true,
719 | "requires": {
720 | "ms": "^2.1.1"
721 | }
722 | }
723 | }
724 | },
725 | "nopt": {
726 | "version": "1.0.10",
727 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
728 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
729 | "dev": true,
730 | "requires": {
731 | "abbrev": "1"
732 | }
733 | },
734 | "normalize-path": {
735 | "version": "3.0.0",
736 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
737 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
738 | "dev": true
739 | },
740 | "normalize-url": {
741 | "version": "4.5.0",
742 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
743 | "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
744 | "dev": true
745 | },
746 | "object-assign": {
747 | "version": "4.1.1",
748 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
749 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
750 | },
751 | "once": {
752 | "version": "1.4.0",
753 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
754 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
755 | "dev": true,
756 | "requires": {
757 | "wrappy": "1"
758 | }
759 | },
760 | "p-cancelable": {
761 | "version": "1.1.0",
762 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
763 | "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
764 | "dev": true
765 | },
766 | "package-json": {
767 | "version": "6.5.0",
768 | "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
769 | "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
770 | "dev": true,
771 | "requires": {
772 | "got": "^9.6.0",
773 | "registry-auth-token": "^4.0.0",
774 | "registry-url": "^5.0.0",
775 | "semver": "^6.2.0"
776 | },
777 | "dependencies": {
778 | "semver": {
779 | "version": "6.3.0",
780 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
781 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
782 | "dev": true
783 | }
784 | }
785 | },
786 | "picomatch": {
787 | "version": "2.2.2",
788 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
789 | "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
790 | "dev": true
791 | },
792 | "prepend-http": {
793 | "version": "2.0.0",
794 | "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
795 | "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
796 | "dev": true
797 | },
798 | "pstree.remy": {
799 | "version": "1.1.8",
800 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
801 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
802 | "dev": true
803 | },
804 | "pump": {
805 | "version": "3.0.0",
806 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
807 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
808 | "dev": true,
809 | "requires": {
810 | "end-of-stream": "^1.1.0",
811 | "once": "^1.3.1"
812 | }
813 | },
814 | "pupa": {
815 | "version": "2.1.1",
816 | "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
817 | "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
818 | "dev": true,
819 | "requires": {
820 | "escape-goat": "^2.0.0"
821 | }
822 | },
823 | "rc": {
824 | "version": "1.2.8",
825 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
826 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
827 | "dev": true,
828 | "requires": {
829 | "deep-extend": "^0.6.0",
830 | "ini": "~1.3.0",
831 | "minimist": "^1.2.0",
832 | "strip-json-comments": "~2.0.1"
833 | }
834 | },
835 | "readdirp": {
836 | "version": "3.5.0",
837 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
838 | "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
839 | "dev": true,
840 | "requires": {
841 | "picomatch": "^2.2.1"
842 | }
843 | },
844 | "registry-auth-token": {
845 | "version": "4.2.1",
846 | "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
847 | "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
848 | "dev": true,
849 | "requires": {
850 | "rc": "^1.2.8"
851 | }
852 | },
853 | "registry-url": {
854 | "version": "5.1.0",
855 | "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
856 | "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
857 | "dev": true,
858 | "requires": {
859 | "rc": "^1.2.8"
860 | }
861 | },
862 | "responselike": {
863 | "version": "1.0.2",
864 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
865 | "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
866 | "dev": true,
867 | "requires": {
868 | "lowercase-keys": "^1.0.0"
869 | }
870 | },
871 | "semver": {
872 | "version": "5.7.1",
873 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
874 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
875 | "dev": true
876 | },
877 | "semver-diff": {
878 | "version": "3.1.1",
879 | "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
880 | "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
881 | "dev": true,
882 | "requires": {
883 | "semver": "^6.3.0"
884 | },
885 | "dependencies": {
886 | "semver": {
887 | "version": "6.3.0",
888 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
889 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
890 | "dev": true
891 | }
892 | }
893 | },
894 | "signal-exit": {
895 | "version": "3.0.3",
896 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
897 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
898 | "dev": true
899 | },
900 | "socket.io": {
901 | "version": "3.0.4",
902 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.4.tgz",
903 | "integrity": "sha512-Vj1jUoO75WGc9txWd311ZJJqS9Dr8QtNJJ7gk2r7dcM/yGe9sit7qOijQl3GAwhpBOz/W8CwkD7R6yob07nLbA==",
904 | "requires": {
905 | "@types/cookie": "^0.4.0",
906 | "@types/cors": "^2.8.8",
907 | "@types/node": "^14.14.7",
908 | "accepts": "~1.3.4",
909 | "base64id": "~2.0.0",
910 | "debug": "~4.1.0",
911 | "engine.io": "~4.0.0",
912 | "socket.io-adapter": "~2.0.3",
913 | "socket.io-parser": "~4.0.1"
914 | }
915 | },
916 | "socket.io-adapter": {
917 | "version": "2.0.3",
918 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz",
919 | "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ=="
920 | },
921 | "socket.io-parser": {
922 | "version": "4.0.2",
923 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz",
924 | "integrity": "sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g==",
925 | "requires": {
926 | "@types/component-emitter": "^1.2.10",
927 | "component-emitter": "~1.3.0",
928 | "debug": "~4.1.0"
929 | }
930 | },
931 | "string-width": {
932 | "version": "4.2.0",
933 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
934 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
935 | "dev": true,
936 | "requires": {
937 | "emoji-regex": "^8.0.0",
938 | "is-fullwidth-code-point": "^3.0.0",
939 | "strip-ansi": "^6.0.0"
940 | },
941 | "dependencies": {
942 | "ansi-regex": {
943 | "version": "5.0.0",
944 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
945 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
946 | "dev": true
947 | },
948 | "emoji-regex": {
949 | "version": "8.0.0",
950 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
951 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
952 | "dev": true
953 | },
954 | "is-fullwidth-code-point": {
955 | "version": "3.0.0",
956 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
957 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
958 | "dev": true
959 | },
960 | "strip-ansi": {
961 | "version": "6.0.0",
962 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
963 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
964 | "dev": true,
965 | "requires": {
966 | "ansi-regex": "^5.0.0"
967 | }
968 | }
969 | }
970 | },
971 | "strip-ansi": {
972 | "version": "5.2.0",
973 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
974 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
975 | "dev": true,
976 | "requires": {
977 | "ansi-regex": "^4.1.0"
978 | }
979 | },
980 | "strip-json-comments": {
981 | "version": "2.0.1",
982 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
983 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
984 | "dev": true
985 | },
986 | "supports-color": {
987 | "version": "5.5.0",
988 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
989 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
990 | "dev": true,
991 | "requires": {
992 | "has-flag": "^3.0.0"
993 | }
994 | },
995 | "term-size": {
996 | "version": "2.2.1",
997 | "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
998 | "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
999 | "dev": true
1000 | },
1001 | "to-readable-stream": {
1002 | "version": "1.0.0",
1003 | "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
1004 | "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
1005 | "dev": true
1006 | },
1007 | "to-regex-range": {
1008 | "version": "5.0.1",
1009 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1010 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1011 | "dev": true,
1012 | "requires": {
1013 | "is-number": "^7.0.0"
1014 | }
1015 | },
1016 | "touch": {
1017 | "version": "3.1.0",
1018 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
1019 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
1020 | "dev": true,
1021 | "requires": {
1022 | "nopt": "~1.0.10"
1023 | }
1024 | },
1025 | "type-fest": {
1026 | "version": "0.8.1",
1027 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
1028 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
1029 | "dev": true
1030 | },
1031 | "typedarray-to-buffer": {
1032 | "version": "3.1.5",
1033 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
1034 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
1035 | "dev": true,
1036 | "requires": {
1037 | "is-typedarray": "^1.0.0"
1038 | }
1039 | },
1040 | "undefsafe": {
1041 | "version": "2.0.3",
1042 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz",
1043 | "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==",
1044 | "dev": true,
1045 | "requires": {
1046 | "debug": "^2.2.0"
1047 | },
1048 | "dependencies": {
1049 | "debug": {
1050 | "version": "2.6.9",
1051 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1052 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1053 | "dev": true,
1054 | "requires": {
1055 | "ms": "2.0.0"
1056 | }
1057 | },
1058 | "ms": {
1059 | "version": "2.0.0",
1060 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1061 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
1062 | "dev": true
1063 | }
1064 | }
1065 | },
1066 | "unique-string": {
1067 | "version": "2.0.0",
1068 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
1069 | "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
1070 | "dev": true,
1071 | "requires": {
1072 | "crypto-random-string": "^2.0.0"
1073 | }
1074 | },
1075 | "update-notifier": {
1076 | "version": "4.1.3",
1077 | "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
1078 | "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==",
1079 | "dev": true,
1080 | "requires": {
1081 | "boxen": "^4.2.0",
1082 | "chalk": "^3.0.0",
1083 | "configstore": "^5.0.1",
1084 | "has-yarn": "^2.1.0",
1085 | "import-lazy": "^2.1.0",
1086 | "is-ci": "^2.0.0",
1087 | "is-installed-globally": "^0.3.1",
1088 | "is-npm": "^4.0.0",
1089 | "is-yarn-global": "^0.3.0",
1090 | "latest-version": "^5.0.0",
1091 | "pupa": "^2.0.1",
1092 | "semver-diff": "^3.1.1",
1093 | "xdg-basedir": "^4.0.0"
1094 | }
1095 | },
1096 | "url-parse-lax": {
1097 | "version": "3.0.0",
1098 | "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
1099 | "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
1100 | "dev": true,
1101 | "requires": {
1102 | "prepend-http": "^2.0.0"
1103 | }
1104 | },
1105 | "vary": {
1106 | "version": "1.1.2",
1107 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1108 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
1109 | },
1110 | "widest-line": {
1111 | "version": "3.1.0",
1112 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
1113 | "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
1114 | "dev": true,
1115 | "requires": {
1116 | "string-width": "^4.0.0"
1117 | }
1118 | },
1119 | "wrappy": {
1120 | "version": "1.0.2",
1121 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1122 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
1123 | "dev": true
1124 | },
1125 | "write-file-atomic": {
1126 | "version": "3.0.3",
1127 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
1128 | "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
1129 | "dev": true,
1130 | "requires": {
1131 | "imurmurhash": "^0.1.4",
1132 | "is-typedarray": "^1.0.0",
1133 | "signal-exit": "^3.0.2",
1134 | "typedarray-to-buffer": "^3.1.5"
1135 | }
1136 | },
1137 | "ws": {
1138 | "version": "7.4.1",
1139 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
1140 | "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ=="
1141 | },
1142 | "xdg-basedir": {
1143 | "version": "4.0.0",
1144 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
1145 | "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
1146 | "dev": true
1147 | }
1148 | }
1149 | }
1150 |
--------------------------------------------------------------------------------
/pictionary-clone/after/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "devStart": "nodemon server.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "socket.io": "^3.0.4"
15 | },
16 | "devDependencies": {
17 | "nodemon": "^2.0.6"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pictionary-clone/after/server/server.js:
--------------------------------------------------------------------------------
1 | const production = process.env.NODE_ENV === "production"
2 | const clientUrl = production ? "realsite.com" : "http://localhost:1234"
3 |
4 | const io = require("socket.io")(3000, {
5 | cors: {
6 | origin: clientUrl
7 | }
8 | })
9 |
10 | const rooms = {}
11 | const WORDS = ["Dog", "Bike", "Human"]
12 |
13 | io.on("connection", socket => {
14 | socket.on("join-room", data => {
15 | const user = { id: socket.id, name: data.name, socket: socket }
16 | let room = rooms[data.roomId]
17 | if (room == null) {
18 | room = { users: [], id: data.roomId }
19 | rooms[data.roomId] = room
20 | }
21 |
22 | room.users.push(user)
23 | socket.join(room.id)
24 |
25 | socket.on("ready", () => {
26 | user.ready = true
27 | if (room.users.every(u => u.ready)) {
28 | room.word = getRandomEntry(WORDS)
29 | room.guesser = getRandomEntry(room.users)
30 | io.to(room.guesser.id).emit("start-drawer", room.word)
31 | room.guesser.socket.to(room.id).emit("start-guesser")
32 | }
33 | })
34 |
35 | socket.on("make-guess", data => {
36 | socket.to(room.id).emit("guess", user.name, data.guess)
37 | if (data.guess.toLowerCase().trim() === room.word.toLowerCase()) {
38 | io.to(room.id).emit("winner", user.name, room.word)
39 | room.users.forEach(u => {
40 | u.ready = false
41 | })
42 | }
43 | })
44 |
45 | socket.on("draw", data => {
46 | socket.to(room.id).emit("draw-line", data.start, data.end)
47 | })
48 |
49 | socket.on("disconnect", () => {
50 | room.users = room.users.filter(u => u !== user)
51 | })
52 | })
53 | })
54 |
55 | function getRandomEntry(array) {
56 | return array[Math.floor(Math.random() * array.length)]
57 | }
58 |
--------------------------------------------------------------------------------
/pictionary-clone/before/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | min-height: 100vh;
10 | margin: 0;
11 | }
12 |
13 | .submit-btn {
14 | width: 100%;
15 | color: white;
16 | background-color: #333;
17 | border: none;
18 | cursor: pointer;
19 | padding: .5rem 1rem;
20 | }
21 |
22 | .submit-btn:hover {
23 | background-color: #555;
24 | }
25 |
26 | form {
27 | max-width: 300px;
28 | width: 100%;
29 | }
30 |
31 | .form-group {
32 | display: flex;
33 | flex-direction: column;
34 | margin-bottom: .75rem;
35 | }
36 |
37 | .form-group > label {
38 | font-weight: bold;
39 | font-size: .8rem;
40 | color: #555;
41 | }
42 |
43 | .form-group > input {
44 | font-size: 1rem;
45 | }
--------------------------------------------------------------------------------
/pictionary-clone/before/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pictionary
8 |
9 |
10 |
11 |
22 |
23 |
--------------------------------------------------------------------------------
/pictionary-clone/before/room.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | padding: .5rem;
7 | margin: 0;
8 | height: 100vh;
9 | display: flex;
10 | background-color: #333;
11 | }
12 |
13 | .game-board {
14 | height: 100%;
15 | width: 100%;
16 | background-color: white;
17 | display: block;
18 | }
19 |
20 | .text-section {
21 | margin-left: .5rem;
22 | display: flex;
23 | flex-direction: column;
24 | flex-shrink: 0;
25 | flex-grow: 0;
26 | width: 170px;
27 | }
28 |
29 | .word {
30 | margin: 0;
31 | margin-bottom: .25rem;
32 | color: white;
33 | text-align: center;
34 | font-weight: normal;
35 | }
36 |
37 | .messages {
38 | flex-grow: 1;
39 | min-height: 5rem;
40 | display: flex;
41 | flex-direction: column;
42 | justify-content: flex-end;
43 | overflow: hidden;
44 | }
45 |
46 | .guess-form {
47 | display: flex;
48 | flex-direction: column;
49 | }
50 |
51 | .guess-input {
52 | margin: .25rem 0;
53 | }
54 |
55 | .guess-btn {
56 | cursor: pointer;
57 | }
58 |
59 | .ready-btn {
60 | cursor: pointer;
61 | }
62 |
63 | .guess-container {
64 | background-color: #CCC;
65 | border-radius: .25rem;
66 | padding: .4rem;
67 | width: 100%;
68 | margin-bottom: .25rem;
69 | }
70 |
71 | .guess-container > .name {
72 | font-weight: bold;
73 | font-size: .75rem;
74 | color: #555;
75 | word-break: break-all;
76 | }
77 |
78 | .guess-container > .text {
79 | word-break: break-all;
80 | }
81 |
82 | @media (max-width: 500px) {
83 | body {
84 | flex-direction: column;
85 | }
86 |
87 | .text-section {
88 | margin-left: 0;
89 | margin-top: .5rem;
90 | width: 100%;
91 | }
92 | }
93 |
94 | .hide {
95 | display: none !important;
96 | }
--------------------------------------------------------------------------------
/pictionary-clone/before/room.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pictionary
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tooltip/after/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .cache
--------------------------------------------------------------------------------
/tooltip/after/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 | Hover Me.
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tooltip/after/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "current-project-2",
3 | "version": "1.0.0",
4 | "main": "script.js",
5 | "scripts": {
6 | "start": "parcel index.html",
7 | "build": "parcel build index.html"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "description": "",
13 | "devDependencies": {
14 | "parcel-bundler": "^1.12.4"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tooltip/after/script.js:
--------------------------------------------------------------------------------
1 | import "./tooltip"
2 |
3 | document.addEventListener("keydown", e => {
4 | const container = document.querySelector(".container")
5 |
6 | switch (e.key) {
7 | case "ArrowLeft":
8 | container.classList.replace("x-center", "x-left")
9 | container.classList.replace("x-right", "x-center")
10 | break
11 | case "ArrowRight":
12 | container.classList.replace("x-center", "x-right")
13 | container.classList.replace("x-left", "x-center")
14 | break
15 | case "ArrowUp":
16 | container.classList.replace("y-center", "y-top")
17 | container.classList.replace("y-bottom", "y-center")
18 | break
19 | case "ArrowDown":
20 | container.classList.replace("y-center", "y-bottom")
21 | container.classList.replace("y-top", "y-center")
22 | break
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/tooltip/after/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | .container {
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | height: 100vh;
10 | padding: 2rem;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | }
16 |
17 | .container.x-left {
18 | justify-content: flex-start;
19 | }
20 |
21 | .container.x-right {
22 | justify-content: flex-end;
23 | }
24 |
25 | .container.x-center {
26 | justify-content: center;
27 | }
28 |
29 | .container.y-top {
30 | align-items: flex-start;
31 | }
32 |
33 | .container.y-bottom {
34 | align-items: flex-end;
35 | }
36 |
37 | .container.y-center {
38 | align-items: center;
39 | }
40 |
41 | .help-text {
42 | border-bottom: 3px dotted blue;
43 | }
44 |
45 | .tooltip-container {
46 | position: fixed;
47 | pointer-events: none;
48 | margin: 0;
49 | top: 0px;
50 | left: 0px;
51 | right: 0px;
52 | bottom: 0px;
53 | }
54 |
55 | .tooltip {
56 | position: absolute;
57 | padding: .4rem;
58 | border: 1px solid black;
59 | border-radius: .25rem;
60 | font-size: .75rem;
61 | max-width: 200px;
62 | }
--------------------------------------------------------------------------------
/tooltip/after/tooltip.js:
--------------------------------------------------------------------------------
1 | import addGlobalEventListener from "./utils/addGlobalEventListener"
2 |
3 | const DEFAULT_SPACING = 5
4 | const POSITION_ORDER = ["top", "bottom", "left", "right"]
5 | const POSITION_TO_FUNCTION_MAP = {
6 | top: positionTooltipTop,
7 | bottom: positionTooltipBottom,
8 | left: positionTooltipLeft,
9 | right: positionTooltipRight
10 | }
11 |
12 | const tooltipContainer = document.createElement("div")
13 | tooltipContainer.classList.add("tooltip-container")
14 | document.body.append(tooltipContainer)
15 |
16 | addGlobalEventListener("mouseover", "[data-tooltip]", e => {
17 | const tooltip = createTooltipElement(e.target.dataset.tooltip)
18 | tooltipContainer.append(tooltip)
19 | positionTooltip(tooltip, e.target)
20 |
21 | e.target.addEventListener(
22 | "mouseleave",
23 | () => {
24 | tooltip.remove()
25 | },
26 | { once: true }
27 | )
28 | })
29 |
30 | // over the top of the element
31 |
32 | function createTooltipElement(text) {
33 | const tooltip = document.createElement("div")
34 | tooltip.classList.add("tooltip")
35 | tooltip.innerText = text
36 | return tooltip
37 | }
38 |
39 | function positionTooltip(tooltip, element) {
40 | const elementRect = element.getBoundingClientRect()
41 | const preferredPositions = (element.dataset.positions || "").split("|")
42 | const spacing = parseInt(element.dataset.spacing) || DEFAULT_SPACING
43 | const positions = preferredPositions.concat(POSITION_ORDER)
44 |
45 | for (let i = 0; i < positions.length; i++) {
46 | const func = POSITION_TO_FUNCTION_MAP[positions[i]]
47 | if (func && func(tooltip, elementRect, spacing)) return
48 | }
49 | }
50 |
51 | function positionTooltipTop(tooltip, elementRect, spacing) {
52 | const tooltipRect = tooltip.getBoundingClientRect()
53 | tooltip.style.top = `${elementRect.top - tooltipRect.height - spacing}px`
54 | tooltip.style.left = `${elementRect.left +
55 | elementRect.width / 2 -
56 | tooltipRect.width / 2}px`
57 |
58 | const bounds = isOutOfBounds(tooltip, spacing)
59 |
60 | if (bounds.top) {
61 | resetTooltipPosition(tooltip)
62 | return false
63 | }
64 | if (bounds.right) {
65 | tooltip.style.right = `${spacing}px`
66 | tooltip.style.left = "initial"
67 | }
68 | if (bounds.left) {
69 | tooltip.style.left = `${spacing}px`
70 | }
71 |
72 | return true
73 | }
74 |
75 | function positionTooltipBottom(tooltip, elementRect, spacing) {
76 | const tooltipRect = tooltip.getBoundingClientRect()
77 | tooltip.style.top = `${elementRect.bottom + spacing}px`
78 | tooltip.style.left = `${elementRect.left +
79 | elementRect.width / 2 -
80 | tooltipRect.width / 2}px`
81 |
82 | const bounds = isOutOfBounds(tooltip, spacing)
83 |
84 | if (bounds.bottom) {
85 | resetTooltipPosition(tooltip)
86 | return false
87 | }
88 | if (bounds.right) {
89 | tooltip.style.right = `${spacing}px`
90 | tooltip.style.left = "initial"
91 | }
92 | if (bounds.left) {
93 | tooltip.style.left = `${spacing}px`
94 | }
95 |
96 | return true
97 | }
98 |
99 | function positionTooltipLeft(tooltip, elementRect, spacing) {
100 | const tooltipRect = tooltip.getBoundingClientRect()
101 | tooltip.style.top = `${elementRect.top +
102 | elementRect.height / 2 -
103 | tooltipRect.height / 2}px`
104 | tooltip.style.left = `${elementRect.left - tooltipRect.width - spacing}px`
105 |
106 | const bounds = isOutOfBounds(tooltip, spacing)
107 |
108 | if (bounds.left) {
109 | resetTooltipPosition(tooltip)
110 | return false
111 | }
112 | if (bounds.bottom) {
113 | tooltip.style.bottom = `${spacing}px`
114 | tooltip.style.top = "initial"
115 | }
116 | if (bounds.top) {
117 | tooltip.style.top = `${spacing}px`
118 | }
119 |
120 | return true
121 | }
122 |
123 | function positionTooltipRight(tooltip, elementRect, spacing) {
124 | const tooltipRect = tooltip.getBoundingClientRect()
125 | tooltip.style.top = `${elementRect.top +
126 | elementRect.height / 2 -
127 | tooltipRect.height / 2}px`
128 | tooltip.style.left = `${elementRect.right + spacing}px`
129 |
130 | const bounds = isOutOfBounds(tooltip, spacing)
131 |
132 | if (bounds.right) {
133 | resetTooltipPosition(tooltip)
134 | return false
135 | }
136 | if (bounds.bottom) {
137 | tooltip.style.bottom = `${spacing}px`
138 | tooltip.style.top = "initial"
139 | }
140 | if (bounds.top) {
141 | tooltip.style.top = `${spacing}px`
142 | }
143 |
144 | return true
145 | }
146 |
147 | function isOutOfBounds(element, spacing) {
148 | const rect = element.getBoundingClientRect()
149 | const containerRect = tooltipContainer.getBoundingClientRect()
150 |
151 | return {
152 | left: rect.left <= containerRect.left + spacing,
153 | right: rect.right >= containerRect.right - spacing,
154 | top: rect.top <= containerRect.top + spacing,
155 | bottom: rect.bottom >= containerRect.bottom - spacing
156 | }
157 | }
158 |
159 | function resetTooltipPosition(tooltip) {
160 | tooltip.style.left = "initial"
161 | tooltip.style.right = "initial"
162 | tooltip.style.top = "initial"
163 | tooltip.style.bottom = "initial"
164 | }
165 |
--------------------------------------------------------------------------------
/tooltip/after/utils/addGlobalEventListener.js:
--------------------------------------------------------------------------------
1 | export default function addGlobalEventListener(type, selector, callback) {
2 | document.addEventListener(type, e => {
3 | if (e.target.matches(selector)) callback(e)
4 | })
5 | }
6 |
--------------------------------------------------------------------------------
/tooltip/before/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 | Hover Me.
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tooltip/before/script.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("keydown", e => {
2 | const container = document.querySelector(".container")
3 |
4 | switch (e.key) {
5 | case "ArrowLeft":
6 | container.classList.replace("x-center", "x-left")
7 | container.classList.replace("x-right", "x-center")
8 | break
9 | case "ArrowRight":
10 | container.classList.replace("x-center", "x-right")
11 | container.classList.replace("x-left", "x-center")
12 | break
13 | case "ArrowUp":
14 | container.classList.replace("y-center", "y-top")
15 | container.classList.replace("y-bottom", "y-center")
16 | break
17 | case "ArrowDown":
18 | container.classList.replace("y-center", "y-bottom")
19 | container.classList.replace("y-top", "y-center")
20 | break
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/tooltip/before/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | .container {
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | height: 100vh;
10 | padding: 2rem;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | }
16 |
17 | .container.x-left {
18 | justify-content: flex-start;
19 | }
20 |
21 | .container.x-right {
22 | justify-content: flex-end;
23 | }
24 |
25 | .container.x-center {
26 | justify-content: center;
27 | }
28 |
29 | .container.y-top {
30 | align-items: flex-start;
31 | }
32 |
33 | .container.y-bottom {
34 | align-items: flex-end;
35 | }
36 |
37 | .container.y-center {
38 | align-items: center;
39 | }
40 |
41 | .help-text {
42 | border-bottom: 3px dotted blue;
43 | }
44 |
45 | .tooltip-container {
46 | position: fixed;
47 | pointer-events: none;
48 | margin: 0;
49 | top: 0px;
50 | left: 0px;
51 | right: 0px;
52 | bottom: 0px;
53 | }
54 |
55 | .tooltip {
56 | position: absolute;
57 | padding: .4rem;
58 | border: 1px solid black;
59 | border-radius: .25rem;
60 | font-size: .75rem;
61 | max-width: 200px;
62 | }
--------------------------------------------------------------------------------
/trello-clone/after/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | dist
3 | node_modules
--------------------------------------------------------------------------------
/trello-clone/after/dragAndDrop.js:
--------------------------------------------------------------------------------
1 | import addGlobalEventListener from "./utils/addGlobalEventListener.js"
2 |
3 | export default function setup(onDragComplete) {
4 | addGlobalEventListener("mousedown", "[data-draggable]", e => {
5 | const selectedItem = e.target
6 | const itemClone = selectedItem.cloneNode(true)
7 | const ghost = selectedItem.cloneNode()
8 | const offset = setupDragItems(selectedItem, itemClone, ghost, e)
9 | setupDragEvents(selectedItem, itemClone, ghost, offset, onDragComplete)
10 | })
11 | }
12 |
13 | function setupDragItems(selectedItem, itemClone, ghost, e) {
14 | const originalRect = selectedItem.getBoundingClientRect()
15 | const offset = {
16 | x: e.clientX - originalRect.left,
17 | y: e.clientY - originalRect.top,
18 | }
19 |
20 | selectedItem.classList.add("hide")
21 |
22 | itemClone.style.width = `${originalRect.width}px`
23 | itemClone.classList.add("dragging")
24 | positionClone(itemClone, e, offset)
25 | document.body.append(itemClone)
26 |
27 | ghost.style.height = `${originalRect.height}px`
28 | ghost.classList.add("ghost")
29 | ghost.innerHTML = ""
30 | selectedItem.parentElement.insertBefore(ghost, selectedItem)
31 |
32 | return offset
33 | }
34 |
35 | function setupDragEvents(
36 | selectedItem,
37 | itemClone,
38 | ghost,
39 | offset,
40 | onDragComplete
41 | ) {
42 | const mouseMoveFunction = e => {
43 | const dropZone = getDropZone(e.target)
44 | positionClone(itemClone, e, offset)
45 | if (dropZone == null) return
46 | const closestChild = Array.from(dropZone.children).find(child => {
47 | const rect = child.getBoundingClientRect()
48 | return e.clientY < rect.top + rect.height / 2
49 | })
50 | if (closestChild != null) {
51 | dropZone.insertBefore(ghost, closestChild)
52 | } else {
53 | dropZone.append(ghost)
54 | }
55 | }
56 |
57 | document.addEventListener("mousemove", mouseMoveFunction)
58 | document.addEventListener(
59 | "mouseup",
60 | () => {
61 | document.removeEventListener("mousemove", mouseMoveFunction)
62 | const dropZone = getDropZone(ghost)
63 | if (dropZone) {
64 | onDragComplete({
65 | startZone: getDropZone(selectedItem),
66 | endZone: dropZone,
67 | dragElement: selectedItem,
68 | index: Array.from(dropZone.children).indexOf(ghost),
69 | })
70 | dropZone.insertBefore(selectedItem, ghost)
71 | }
72 |
73 | stopDrag(selectedItem, itemClone, ghost)
74 | },
75 | { once: true }
76 | )
77 | }
78 |
79 | function positionClone(itemClone, mousePosition, offset) {
80 | itemClone.style.top = `${mousePosition.clientY - offset.y}px`
81 | itemClone.style.left = `${mousePosition.clientX - offset.x}px`
82 | }
83 |
84 | function stopDrag(selectedItem, itemClone, ghost) {
85 | selectedItem.classList.remove("hide")
86 | itemClone.remove()
87 | ghost.remove()
88 | }
89 |
90 | function getDropZone(element) {
91 | if (element.matches("[data-drop-zone]")) {
92 | return element
93 | } else {
94 | return element.closest("[data-drop-zone]")
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/trello-clone/after/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trello Clone
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/trello-clone/after/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "current-project-2",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "parcel-bundler": "^1.12.4"
15 | },
16 | "dependencies": {
17 | "uuid": "^8.3.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/trello-clone/after/script.js:
--------------------------------------------------------------------------------
1 | import addGlobalEventListener from "./utils/addGlobalEventListener.js"
2 | import setupDragAndDrop from "./dragAndDrop.js"
3 | import { v4 as uuidV4 } from "uuid"
4 |
5 | const STORAGE_PREFIX = "TRELLO_CLONE"
6 | const LANES_STORAGE_KEY = `${STORAGE_PREFIX}-lanes`
7 | const DEFAULT_LANES = {
8 | backlog: [{ id: uuidV4(), text: "Create your first task" }],
9 | doing: [],
10 | done: [],
11 | }
12 | const lanes = loadLanes()
13 | renderTasks()
14 |
15 | setupDragAndDrop(onDragComplete)
16 |
17 | addGlobalEventListener("submit", "[data-task-form]", e => {
18 | e.preventDefault()
19 |
20 | const taskInput = e.target.querySelector("[data-task-input]")
21 | const taskText = taskInput.value
22 | if (taskText === "") return
23 |
24 | const task = { id: uuidV4(), text: taskText }
25 | const laneElement = e.target.closest(".lane").querySelector("[data-lane-id]")
26 | lanes[laneElement.dataset.laneId].push(task)
27 |
28 | const taskElement = createTaskElement(task)
29 | laneElement.append(taskElement)
30 | taskInput.value = ""
31 |
32 | saveLanes()
33 | })
34 |
35 | function onDragComplete(e) {
36 | const startLaneId = e.startZone.dataset.laneId
37 | const endLaneId = e.endZone.dataset.laneId
38 | const startLaneTasks = lanes[startLaneId]
39 | const endLaneTasks = lanes[endLaneId]
40 |
41 | const task = startLaneTasks.find(t => t.id === e.dragElement.id)
42 | startLaneTasks.splice(startLaneTasks.indexOf(task), 1)
43 | endLaneTasks.splice(e.index, 0, task)
44 |
45 | saveLanes()
46 | }
47 |
48 | function loadLanes() {
49 | const lanesJson = localStorage.getItem(LANES_STORAGE_KEY)
50 | return JSON.parse(lanesJson) || DEFAULT_LANES
51 | }
52 |
53 | function saveLanes() {
54 | localStorage.setItem(LANES_STORAGE_KEY, JSON.stringify(lanes))
55 | }
56 |
57 | function renderTasks() {
58 | Object.entries(lanes).forEach(obj => {
59 | const laneId = obj[0]
60 | const tasks = obj[1]
61 | const lane = document.querySelector(`[data-lane-id="${laneId}"]`)
62 | tasks.forEach(task => {
63 | const taskElement = createTaskElement(task)
64 | lane.append(taskElement)
65 | })
66 | })
67 | }
68 |
69 | function createTaskElement(task) {
70 | const element = document.createElement("div")
71 | element.id = task.id
72 | element.innerText = task.text
73 | element.classList.add("task")
74 | element.dataset.draggable = true
75 | return element
76 | }
77 |
--------------------------------------------------------------------------------
/trello-clone/after/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | user-select: none;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | background-color: #CCC;
9 | height: 50vh;
10 | overflow: hidden;
11 | }
12 |
13 | .lanes {
14 | display: flex;
15 | justify-content: center;
16 | padding: .5rem;
17 | height: 100%;
18 | }
19 |
20 | .lane {
21 | display: flex;
22 | flex-direction: column;
23 | background-color: #AAA;
24 | flex-basis: 0;
25 | flex-grow: 1;
26 | margin: .25rem;
27 | border-radius: .25rem;
28 | overflow: hidden;
29 | max-width: 16rem;
30 | }
31 |
32 | .header {
33 | text-align: center;
34 | background-color: #333;
35 | color: white;
36 | padding: .25rem;
37 | }
38 |
39 | .tasks {
40 | padding: 0 .25rem;
41 | flex-grow: 1;
42 | overflow-y: auto;
43 | }
44 |
45 | .task {
46 | padding: .25rem;
47 | border-radius: .25rem;
48 | background-color: white;
49 | cursor: grab;
50 | margin: .25rem 0;
51 | text-align: center;
52 | word-wrap: break-word;
53 | }
54 |
55 | .task-input {
56 | padding: .3rem .5rem;
57 | border: none;
58 | background-color: #333;
59 | color: white;
60 | outline: none;
61 | width: 100%;
62 | }
63 |
64 | .task-input::placeholder {
65 | color: #AAA;
66 | }
67 |
68 | [data-draggable] {
69 | user-select: none;
70 | }
71 |
72 | [data-draggable].hide {
73 | display: none !important;
74 | }
75 |
76 | [data-draggable].dragging {
77 | position: absolute;
78 | opacity: .5;
79 | transform: rotate(5deg);
80 | pointer-events: none;
81 | }
82 |
83 | [data-draggable].ghost {
84 | background-color: black;
85 | opacity: .25;
86 | }
--------------------------------------------------------------------------------
/trello-clone/after/utils/addGlobalEventListener.js:
--------------------------------------------------------------------------------
1 | export default function addGlobalEventListener(type, selector, callback) {
2 | document.addEventListener(type, e => {
3 | if (e.target.matches(selector)) callback(e)
4 | })
5 | }
6 |
--------------------------------------------------------------------------------
/trello-clone/before/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Trello Clone
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 | Do Laundry
19 |
20 |
21 | Edit Video
22 |
23 |
24 |
27 |
28 |
29 |
32 |
33 |
34 | Record Video
35 |
36 |
37 |
40 |
41 |
42 |
45 |
46 |
47 | Plan Trello Clone Video
48 |
49 |
50 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/trello-clone/before/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | user-select: none;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | background-color: #CCC;
9 | height: 50vh;
10 | overflow: hidden;
11 | }
12 |
13 | .lanes {
14 | display: flex;
15 | justify-content: center;
16 | padding: .5rem;
17 | height: 100%;
18 | }
19 |
20 | .lane {
21 | display: flex;
22 | flex-direction: column;
23 | background-color: #AAA;
24 | flex-basis: 0;
25 | flex-grow: 1;
26 | margin: .25rem;
27 | border-radius: .25rem;
28 | overflow: hidden;
29 | max-width: 16rem;
30 | }
31 |
32 | .header {
33 | text-align: center;
34 | background-color: #333;
35 | color: white;
36 | padding: .25rem;
37 | }
38 |
39 | .tasks {
40 | padding: 0 .25rem;
41 | flex-grow: 1;
42 | overflow-y: auto;
43 | }
44 |
45 | .task {
46 | padding: .25rem;
47 | border-radius: .25rem;
48 | background-color: white;
49 | cursor: grab;
50 | margin: .25rem 0;
51 | text-align: center;
52 | word-wrap: break-word;
53 | }
54 |
55 | .task-input {
56 | padding: .3rem .5rem;
57 | border: none;
58 | background-color: #333;
59 | color: white;
60 | outline: none;
61 | width: 100%;
62 | }
63 |
64 | .task-input::placeholder {
65 | color: #AAA;
66 | }
67 |
68 | [data-draggable] {
69 | user-select: none;
70 | }
71 |
72 | [data-draggable].hide {
73 | display: none !important;
74 | }
75 |
76 | [data-draggable].dragging {
77 | position: absolute;
78 | opacity: .5;
79 | transform: rotate(5deg);
80 | pointer-events: none;
81 | }
82 |
83 | [data-draggable].ghost {
84 | background-color: black;
85 | opacity: .25;
86 | }
--------------------------------------------------------------------------------