├── .gitignore
├── client
├── app.jsx
├── assets
│ ├── dropdown.svg
│ ├── mountain.svg
│ ├── plus.svg
│ ├── sign.svg
│ ├── star.svg
│ └── x.svg
├── components
│ ├── confirm-delete-prompt.jsx
│ ├── confirm-update-prompt.jsx
│ └── entry.jsx
├── containers
│ ├── entries.jsx
│ ├── form.jsx
│ └── header.jsx
├── index.html
├── index.js
└── stylesheets
│ ├── entries.css
│ ├── form-styles.css
│ ├── header.css
│ ├── styles.css
│ └── title.css
├── docs
└── schema.png
├── package.json
├── readme.md
├── server
├── controllers
│ ├── entry-controller.js
│ └── form-controller.js
├── models
│ ├── entry-queries.js
│ ├── form-queries.js
│ └── model.js
└── server.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | .DS_Store
4 | /client/build/*
--------------------------------------------------------------------------------
/client/app.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react';
2 | import Header from './containers/header.jsx';
3 | import Form from './containers/form.jsx';
4 | import Entries from './containers/entries.jsx';
5 |
6 | const App = () => {
7 | // State of existing entry records
8 | const [ entries, setEntries ] = useState([]);
9 |
10 | // State of form dropdown options
11 | const [ activities, setActivities ] = useState([]);
12 | const [ types, setTypes ] = useState([]);
13 | const [ difficulties, setDifficulties ] = useState([]);
14 |
15 | // State of controlled main form component
16 | const [ selectedActivity, setSelectedActivity ] = useState('');
17 | const [ selectedType, setSelectedType ] = useState('');
18 | const [ selectedDifficulty, setSelectedDifficulty ] = useState('');
19 | const [ currentRoute, setCurrentRoute ] = useState('');
20 | const [ selectedRating, setSelectedRating ] = useState(0);
21 | const [ currentLocation, setCurrentLocation ] = useState('');
22 | const [ currentRegion, setCurrentRegion ] = useState('');
23 | const [ currentCountry, setCurrentCountry ] = useState('');
24 | const [ selectedStartDate, setSelectedStartDate ] = useState('');
25 | const [ selectedEndDate, setSelectedEndDate ] = useState('');
26 | const [ currentNote, setCurrentNote ] = useState('');
27 | const [ starHover, setStarHover ] = useState(0);
28 |
29 | // State of controlled update form component
30 | const [ selectedActivityForUpdate, setSelectedActivityForUpdate ] = useState('');
31 | const [ selectedTypeForUpdate, setSelectedTypeForUpdate ] = useState('');
32 | const [ selectedDifficultyForUpdate, setSelectedDifficultyForUpdate ] = useState('');
33 | const [ currentRouteForUpdate, setCurrentRouteForUpdate ] = useState('');
34 | const [ selectedRatingForUpdate, setSelectedRatingForUpdate ] = useState(0);
35 | const [ currentLocationForUpdate, setCurrentLocationForUpdate ] = useState('');
36 | const [ currentRegionForUpdate, setCurrentRegionForUpdate ] = useState('');
37 | const [ currentCountryForUpdate, setCurrentCountryForUpdate ] = useState('');
38 | const [ selectedStartDateForUpdate, setSelectedStartDateForUpdate ] = useState('');
39 | const [ selectedEndDateForUpdate, setSelectedEndDateForUpdate ] = useState('');
40 | const [ currentNoteForUpdate, setCurrentNoteForUpdate ] = useState('');
41 |
42 | // State of popup prompts
43 | const [ confirmDelete, setConfirmDelete ] = useState(false);
44 | const [ confirmUpdate, setConfirmUpdate ] = useState(false);
45 | const [ displayMainForm, setDisplayMainForm ] = useState(false);
46 |
47 | // Handler to update state of controlled main form component values
48 | const handleFormChange = (e) => {
49 | if (e.target.name === 'activity') {
50 | setSelectedActivity(Number(e.target.value));
51 |
52 | const defaultType = types.filter(type => type.activity_id === Number(e.target.value))[0]._id
53 | setSelectedType(defaultType)
54 |
55 | const defaultDifficulty = difficulties.filter(difficulty => difficulty.type_id === defaultType)[0]._id
56 | setSelectedDifficulty(defaultDifficulty)
57 | }
58 | if (e.target.name === 'type') {
59 | setSelectedType(Number(e.target.value));
60 | setSelectedDifficulty(difficulties.filter(difficulty => difficulty.type_id === Number(e.target.value))[0]._id)
61 | }
62 | if (e.target.name === 'difficulty') setSelectedDifficulty(Number(e.target.value));
63 | if (e.target.name === 'route') setCurrentRoute(e.target.value);
64 | if (e.target.name === 'location')setCurrentLocation(e.target.value);
65 | if (e.target.name === 'region') setCurrentRegion(e.target.value);
66 | if (e.target.name === 'country') setCurrentCountry(e.target.value);
67 | if (e.target.name === 'start-date') setSelectedStartDate(e.target.value);
68 | if (e.target.name === 'end-date') setSelectedEndDate(e.target.value);
69 | if (e.target.name === 'note') setCurrentNote(e.target.value);
70 |
71 | if (e.target.id === 'star-1') setSelectedRating(1);
72 | if (e.target.id === 'star-2') setSelectedRating(2);
73 | if (e.target.id === 'star-3') setSelectedRating(3);
74 | if (e.target.id === 'star-4') setSelectedRating(4);
75 | if (e.target.id === 'star-5') setSelectedRating(5);
76 | if (e.target.id === 'star-clear') setSelectedRating(0);
77 | }
78 |
79 | // Handler to submit controlled main form component values and create new entry record
80 | const handleFormSubmit = async (e) => {
81 | e.preventDefault();
82 |
83 | // Create http request body with controlled form component values
84 | const entryToCreate = {
85 | country: currentCountry,
86 | region: currentRegion,
87 | location: currentLocation,
88 | route: currentRoute,
89 | typeId: selectedType,
90 | difficultyId: selectedDifficulty,
91 | note: currentNote,
92 | startDate: selectedStartDate,
93 | endDate: selectedEndDate === '' ? null : selectedEndDate,
94 | rating: selectedRating
95 | }
96 |
97 | try {
98 | // Send POST request
99 | const response = await fetch('/entries', {
100 | method: 'POST',
101 | headers: {
102 | 'Content-Type': 'application/json'
103 | },
104 | body: JSON.stringify(entryToCreate)
105 | });
106 |
107 | if (response.status !== 201) {
108 | // Throw an error if request is not successful
109 | const error = await response.json();
110 | throw new Error(error.message);
111 | }
112 | else {
113 | // If successful, add the created entry to the state of entry records in the correctly sorted position
114 | const createdEntry = await response.json();
115 |
116 | setEntries([...entries, ...createdEntry].sort((a, b) => {
117 | return new Date(b.start_date) - new Date(a.start_date)
118 | }));
119 |
120 | // Reset controlled form component values
121 | const defaultActivity = activities[0]._id
122 | setSelectedActivity(defaultActivity);
123 |
124 | const defaultType = types.filter(type => type.activity_id === defaultActivity)[0]._id
125 | setSelectedType(defaultType);
126 |
127 | const defaultDifficulty = difficulties.filter(difficulty => difficulty.type_id === defaultType)[0]._id
128 | setSelectedDifficulty(defaultDifficulty);
129 |
130 | setCurrentRoute('');
131 | setSelectedRating(0);
132 | setCurrentLocation('');
133 | setCurrentRegion('');
134 | setCurrentCountry('');
135 | setSelectedStartDate('');
136 | setSelectedEndDate('');
137 | setCurrentNote('');
138 | }
139 | }
140 | catch (err) {
141 | // Console log any caught errors
142 | console.log(err);
143 | }
144 | }
145 |
146 | // Handler to display prompt to confirm update
147 | const handleConfirmUpdate = (entryId) => {
148 | if (entryId) {
149 | const selectedEntryForUpdate = entries.filter(entry => entry.entry_id === entryId)[0];
150 |
151 | setSelectedActivityForUpdate(selectedEntryForUpdate.activity_id);
152 | setSelectedTypeForUpdate(selectedEntryForUpdate.type_id);
153 | setSelectedDifficultyForUpdate(selectedEntryForUpdate.difficulty_id);
154 | setCurrentRouteForUpdate(selectedEntryForUpdate.route);
155 | setSelectedRatingForUpdate(selectedEntryForUpdate.rating);
156 | setCurrentLocationForUpdate(selectedEntryForUpdate.location);
157 | setCurrentRegionForUpdate(selectedEntryForUpdate.region);
158 | setCurrentCountryForUpdate(selectedEntryForUpdate.country);
159 | setSelectedStartDateForUpdate(selectedEntryForUpdate.start_date.substring(0, 10));
160 | setSelectedEndDateForUpdate(selectedEntryForUpdate.end_date ? selectedEntryForUpdate.end_date.substring(0, 10) : '');
161 | setCurrentNoteForUpdate(selectedEntryForUpdate.note);
162 | }
163 |
164 | setConfirmUpdate(entryId);
165 | }
166 |
167 | // Handler to update state of controlled update form component values
168 | const handleUpdateFormChange = (e) => {
169 | if (e.target.name === 'activity') {
170 | setSelectedActivityForUpdate(Number(e.target.value));
171 |
172 | const defaultType = types.filter(type => type.activity_id === Number(e.target.value))[0]._id
173 | setSelectedTypeForUpdate(defaultType)
174 |
175 | const defaultDifficulty = difficulties.filter(difficulty => difficulty.type_id === defaultType)[0]._id
176 | setSelectedDifficultyForUpdate(defaultDifficulty)
177 | }
178 | if (e.target.name === 'type') {
179 | setSelectedTypeForUpdate(Number(e.target.value));
180 | setSelectedDifficultyForUpdate(difficulties.filter(difficulty => difficulty.type_id === Number(e.target.value))[0]._id)
181 | }
182 | if (e.target.name === 'difficulty') setSelectedDifficultyForUpdate(Number(e.target.value));
183 | if (e.target.name === 'route') setCurrentRouteForUpdate(e.target.value);
184 | if (e.target.name === 'location')setCurrentLocationForUpdate(e.target.value);
185 | if (e.target.name === 'region') setCurrentRegionForUpdate(e.target.value);
186 | if (e.target.name === 'country') setCurrentCountryForUpdate(e.target.value);
187 | if (e.target.name === 'start-date') setSelectedStartDateForUpdate(e.target.value);
188 | if (e.target.name === 'end-date') setSelectedEndDateForUpdate(e.target.value);
189 | if (e.target.name === 'note') setCurrentNoteForUpdate(e.target.value);
190 |
191 | if (e.target.id === 'star-1') setSelectedRatingForUpdate(1);
192 | if (e.target.id === 'star-2') setSelectedRatingForUpdate(2);
193 | if (e.target.id === 'star-3') setSelectedRatingForUpdate(3);
194 | if (e.target.id === 'star-4') setSelectedRatingForUpdate(4);
195 | if (e.target.id === 'star-5') setSelectedRatingForUpdate(5);
196 | if (e.target.id === 'star-clear') setSelectedRatingForUpdate(0);
197 | }
198 |
199 | // Handler to update an entry
200 | const handleEntryUpdate = async (e) => {
201 | e.preventDefault();
202 |
203 | const entryToUpdate = {
204 | entryId: confirmUpdate,
205 | country: currentCountryForUpdate,
206 | region: currentRegionForUpdate,
207 | location: currentLocationForUpdate,
208 | route: currentRouteForUpdate,
209 | typeId: selectedTypeForUpdate,
210 | difficultyId: selectedDifficultyForUpdate,
211 | note: currentNoteForUpdate,
212 | startDate: selectedStartDateForUpdate,
213 | endDate: selectedEndDateForUpdate === '' ? null : selectedEndDateForUpdate,
214 | rating: selectedRatingForUpdate
215 | };
216 |
217 | try {
218 | const response = await fetch('/entries', {
219 | method: 'PUT',
220 | headers: {
221 | 'Content-Type': 'application/json'
222 | },
223 | body: JSON.stringify(entryToUpdate)
224 | })
225 |
226 | if (response.status !== 200) {
227 | // Throw an error if request is not successful
228 | const error = await response.json();
229 | throw new Error(error.message);
230 | }
231 | else {
232 | const updatedEntry = await response.json();
233 |
234 | const updatedEntries = [
235 | ...entries.filter(entry => entry.entry_id !== confirmUpdate),
236 | ...updatedEntry
237 | ]
238 |
239 | setEntries(updatedEntries.sort((a, b) => {
240 | return new Date(b.start_date) - new Date(a.start_date)
241 | }));
242 |
243 | setConfirmUpdate(false);
244 | }
245 | }
246 | catch (err) {
247 | console.log(err)
248 | }
249 | }
250 |
251 | // Handler to display prompt to confirm deletion
252 | const handleConfirmDelete = (entryId) => {
253 | setConfirmDelete(entryId);
254 | }
255 |
256 | // Handler to delete an entry
257 | const handleEntryDelete = async (entryId) => {
258 | // Create request body with passed in id of the entry
259 | const entryToDelete = { entryId }
260 |
261 | try {
262 | // Send DELETE request
263 | const response = await fetch('/entries', {
264 | method: 'DELETE',
265 | headers: {
266 | 'Content-Type': 'application/json'
267 | },
268 | body: JSON.stringify(entryToDelete)
269 | })
270 |
271 | if (response.status !== 200) {
272 | // Throw an error if request is not successful
273 | const error = await response.json();
274 | throw new Error(error);
275 | }
276 | else {
277 | // If successful, remove deleted entry from rendered list of entries
278 | setEntries(entries.filter((entry => entry.entry_id !== entryId)));
279 |
280 | // Remove confirm prompt
281 | setConfirmDelete(false);
282 | }
283 | }
284 | catch (err) {
285 | console.log(err);
286 | }
287 | }
288 |
289 | // Toggle handler for main form
290 | const toggleMainForm = () => {
291 | setDisplayMainForm(!displayMainForm);
292 | }
293 |
294 | const handleStarHover = (e) => {
295 | if (e.type === 'mouseenter') {
296 | if (e.target.id === 'star-1') setStarHover(1);
297 | if (e.target.id === 'star-2') setStarHover(2);
298 | if (e.target.id === 'star-3') setStarHover(3);
299 | if (e.target.id === 'star-4') setStarHover(4);
300 | if (e.target.id === 'star-5') setStarHover(5);
301 | }
302 | if (e.type === 'mouseleave') setStarHover(0);
303 | }
304 |
305 | // Fetch and update entry records state on mount
306 | useEffect(() => {
307 | const fetchEntries = async () => {
308 | const response = await fetch('/entries');
309 | const newEntries = await response.json();
310 |
311 | setEntries(newEntries);
312 | }
313 |
314 | fetchEntries();
315 | }, []);
316 |
317 | // Fetch and update form dropdowns state and set initial dropdown selections on mount
318 | useEffect(() => {
319 | const fetchDropdowns = async () => {
320 | const response = await fetch('/form');
321 | const formDropdowns = await response.json();
322 |
323 | setActivities(formDropdowns.activities);
324 | setTypes(formDropdowns.types);
325 | setDifficulties(formDropdowns.difficulties.sort((a, b) => a.sort - b.sort));
326 |
327 | const defaultActivity = formDropdowns.activities[0]._id
328 | setSelectedActivity(defaultActivity);
329 |
330 | const defaultType = formDropdowns.types.filter(type => type.activity_id === defaultActivity)[0]._id
331 | setSelectedType(defaultType);
332 |
333 | const defaultDifficulty = formDropdowns.difficulties.filter(difficulty => difficulty.type_id === defaultType)[0]._id
334 | setSelectedDifficulty(defaultDifficulty);
335 | }
336 |
337 | fetchDropdowns();
338 | }, []);
339 |
340 | // render Header, Form, and Entries containers
341 | return(
342 |
343 |
344 |
345 |
What mountains did you climb today?
346 |

347 |
348 |
370 |
371 |
397 |
398 |
399 | );
400 | };
401 |
402 | export default App;
403 |
--------------------------------------------------------------------------------
/client/assets/dropdown.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/client/assets/mountain.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/client/assets/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/client/assets/sign.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/client/assets/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/client/assets/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/client/components/confirm-delete-prompt.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ConfirmDeletePrompt = (props) => {
4 | const { confirmDelete,
5 | handleEntryDelete,
6 | handleConfirmDelete
7 | } = props;
8 |
9 | const displayStyle = {
10 | display: confirmDelete ? 'flex' : 'none'
11 | }
12 |
13 | return (
14 |
15 |
16 |
Are you sure you want to delete this entry?
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default ConfirmDeletePrompt;
27 |
--------------------------------------------------------------------------------
/client/components/confirm-update-prompt.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from '../containers/form.jsx'
3 |
4 | const ConfirmUpdatePrompt = (props) => {
5 | const {
6 | confirmUpdate,
7 | handleConfirmUpdate,
8 | activities,
9 | types,
10 | difficulties,
11 | selectedActivityForUpdate,
12 | selectedTypeForUpdate,
13 | selectedDifficultyForUpdate,
14 | currentRouteForUpdate,
15 | selectedRatingForUpdate,
16 | currentLocationForUpdate,
17 | currentRegionForUpdate,
18 | currentCountryForUpdate,
19 | selectedStartDateForUpdate,
20 | selectedEndDateForUpdate,
21 | currentNoteForUpdate,
22 | handleUpdateFormChange,
23 | handleEntryUpdate,
24 | handleStarHover,
25 | starHover
26 | } = props;
27 |
28 | const displayStyle = {
29 | display: confirmUpdate ? 'flex' : 'none'
30 | }
31 |
32 | const form = confirmUpdate
33 | ?
55 | : null
56 |
57 | return (
58 |
59 |
60 |
63 | {/*

handleConfirmUpdate(false)} /> */}
64 | {form}
65 |
66 |
67 | );
68 | }
69 |
70 | export default ConfirmUpdatePrompt;
71 |
--------------------------------------------------------------------------------
/client/components/entry.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /* -- ENTRY COMPONENT -- */
4 | const Entry= (props) => {
5 | const {
6 | id,
7 | startYear,
8 | startMonth,
9 | startDay,
10 | endYear,
11 | endMonth,
12 | endDay,
13 | activity,
14 | type,
15 | route,
16 | difficulty,
17 | rating,
18 | location,
19 | region,
20 | country,
21 | note,
22 | timestamp,
23 | displayFirstMonth,
24 | displayLastMonth,
25 | handleConfirmUpdate,
26 | handleConfirmDelete
27 | } = props;
28 |
29 | let starRating = [];
30 |
31 | for (let i = 0; i < rating; i++) {
32 | starRating.push(
33 |
36 | );
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 | {
44 | displayFirstMonth
45 | ? endMonth
46 | ? endMonth
47 | : startMonth
48 | : null
49 | }
50 | {
51 | displayFirstMonth
52 | ? endYear
53 | ? endYear
54 | : startYear
55 | : null
56 | }
57 |
58 |
59 |
60 | {endDay ? endDay : startDay}
61 |
62 |
63 |
64 |
{`${location} - ${region}, ${country}`}
65 | {activity}
66 | {`${route} - ${type} (${difficulty})`}
67 | {starRating}
68 |
69 |
70 |
71 | {displayLastMonth ? startMonth : null}
72 | {displayLastMonth ? startYear : null}
73 |
74 |
75 |
76 | {endDay ? startDay : endDay}
77 |
78 |
79 |
80 |
{note}
81 |
{`Posted on ${timestamp}`}
82 |
83 |
84 |
92 |
93 |
100 |
101 | );
102 | }
103 |
104 | export default Entry;
105 |
--------------------------------------------------------------------------------
/client/containers/entries.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Entry from '../components/entry.jsx';
3 | import ConfirmDeletePrompt from '../components/confirm-delete-prompt.jsx';
4 | import ConfirmUpdatePrompt from '../components/confirm-update-prompt.jsx';
5 |
6 | const Entries = (props) => {
7 | // Destructure entries from state
8 | const {
9 | entries,
10 | confirmDelete,
11 | handleConfirmDelete,
12 | handleEntryDelete,
13 | confirmUpdate,
14 | handleConfirmUpdate,
15 | activities,
16 | types,
17 | difficulties,
18 | selectedActivityForUpdate,
19 | selectedTypeForUpdate,
20 | selectedDifficultyForUpdate,
21 | currentRouteForUpdate,
22 | selectedRatingForUpdate,
23 | currentLocationForUpdate,
24 | currentRegionForUpdate,
25 | currentCountryForUpdate,
26 | selectedStartDateForUpdate,
27 | selectedEndDateForUpdate,
28 | currentNoteForUpdate,
29 | handleUpdateFormChange,
30 | handleEntryUpdate,
31 | handleStarHover,
32 | starHover
33 | } = props;
34 |
35 | let previousStartMonth;
36 | let previousEndMonth;
37 |
38 | const entryItemList = entries.map(entry => {
39 | const startDate = new Date(entry.start_date);
40 | const endDate = entry.end_date !== null ? new Date(entry.end_date) : null;
41 | const timestamp = new Date(entry.post_date).toLocaleString();
42 |
43 | let displayFirstMonth;
44 | let displayLastMonth;
45 | const currentStartMonth = startDate.getMonth()
46 | const currentEndMonth = endDate !== null ? endDate.getMonth() : null
47 |
48 | if (currentEndMonth === null) {
49 | if (previousStartMonth === undefined || previousStartMonth !== currentStartMonth) displayFirstMonth = true;
50 | else displayFirstMonth = false;
51 | displayLastMonth = false;
52 | }
53 | if (currentEndMonth !== null) {
54 | if (currentStartMonth !== currentEndMonth) displayLastMonth = true
55 | else displayLastMonth = false;
56 |
57 | if (previousStartMonth === undefined || currentEndMonth !== previousStartMonth) displayFirstMonth = true;
58 | else displayFirstMonth = false;
59 | }
60 |
61 | previousStartMonth = currentStartMonth;
62 | previousEndMonth = currentEndMonth;
63 |
64 | return (
65 |
89 | );
90 | });
91 |
92 | // render entry items
93 | return(
94 |
95 | {entryItemList}
96 |
118 |
123 |
124 | );
125 | }
126 |
127 | export default Entries;
128 |
--------------------------------------------------------------------------------
/client/containers/form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /* -- FORM CONTAINER -- */
4 | const Form = (props) => {
5 | // Destructure drilled props: dropdown options, controlled form component values, handlers
6 | const {
7 | activities,
8 | types,
9 | difficulties,
10 | selectedActivity,
11 | selectedType,
12 | selectedDifficulty,
13 | currentRoute,
14 | selectedRating,
15 | currentLocation,
16 | currentRegion,
17 | currentCountry,
18 | selectedStartDate,
19 | selectedEndDate,
20 | currentNote,
21 | handleFormChange,
22 | handleFormSubmit,
23 | displayMainForm,
24 | purpose,
25 | handleStarHover,
26 | starHover
27 | } = props;
28 |
29 | // Create dropdown options for activities, types and difficulties based on state values
30 | const activitiesList = activities.map(activity => {
31 | return (
32 |
33 | );
34 | })
35 |
36 | const typesList = types.filter(type => type.activity_id === Number(selectedActivity))
37 | .map(type => {
38 | return (
39 |
40 | );
41 | });
42 |
43 | const difficultiesList = difficulties.filter(difficulty => difficulty.type_id === Number(selectedType))
44 | .map(difficulty => {
45 | return (
46 |
47 | );
48 | })
49 |
50 | // Set placeholder value for route field of form based on the currently selected type
51 | let convention;
52 | if (types[0]) convention = types.filter(type => type._id === Number(selectedType))[0].convention;
53 |
54 | const displayStyle = {
55 | maxHeight: displayMainForm ? '475px' : '0px',
56 | backgroundColor: purpose === 'main' ? '#e8e0d5' : 'rgb(244,242,237)'
57 | }
58 |
59 | const submitText = purpose === 'main' ? 'LOG' : 'FIX';
60 |
61 | // set up star ratings for form
62 | let starRating = [];
63 | for (let i = 1; i <= 5; i++) {
64 | starRating.push(
65 |
79 | );
80 | }
81 |
82 | if (selectedRating !== 0) starRating.push(
83 | Clear
84 | );
85 |
86 | // Render component with dropdown lists, controlled form component values and handlers
87 | return (
88 |
202 | );
203 | }
204 |
205 | export default Form;
206 |
--------------------------------------------------------------------------------
/client/containers/header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /* -- HEADER CONTAINER -- */
4 | const Header = (props) => {
5 | return(
6 |
12 | );
13 | }
14 |
15 | export default Header;
16 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Out & About
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './app.jsx'
4 |
5 | const root = createRoot(document.getElementById('root'));
6 | root.render()
7 |
--------------------------------------------------------------------------------
/client/stylesheets/entries.css:
--------------------------------------------------------------------------------
1 | /* entry item grid layout */
2 | #entry-container {
3 | width: 900px;
4 | margin: auto;
5 | }
6 |
7 | .entry {
8 | display: grid;
9 | grid-template-columns: 80px 60px 660px 50px 50px;
10 | border-top: 2px solid #e8e0d5;
11 | }
12 |
13 | /* start month/year */
14 | .month-banner-start {
15 | background: url(../assets/sign.svg);
16 | background-size: 100px;
17 | background-repeat: no-repeat;
18 | }
19 |
20 | .month-banner-end {
21 | background: url(../assets/sign.svg);
22 | background-size: 100px;
23 | background-repeat: no-repeat;
24 | background-position: bottom left;
25 | }
26 |
27 | .entry-start-container {
28 | display: flex;
29 | flex-direction: column;
30 | justify-content: start;
31 | align-items: center;
32 | grid-column: 1;
33 | }
34 |
35 | .entry-start-container span {
36 | position: relative;
37 | top: 42px;
38 | left: 1px;
39 | display: block;
40 | font-family: Arial, Helvetica, sans-serif;
41 | color: rgb(120,120,120);
42 | }
43 |
44 | .entry-start-month {
45 | font-size: 18px;
46 | }
47 |
48 | .entry-start-year {
49 | font-size: 11px;
50 | }
51 |
52 | /* start day */
53 | .entry-start-day {
54 | display: flex;
55 | flex-direction: column;
56 | justify-content: start;
57 | align-items: center;
58 | grid-column: 2;
59 | grid-row: 1;
60 | font-weight: bold;
61 | color: rgb(44,44,44);
62 | }
63 |
64 | .entry-start-day span {
65 | position: relative;
66 | top: 43px;
67 | display: block;
68 | font-family: Arial, Helvetica, sans-serif;
69 | font-size: 28px;
70 | }
71 |
72 | /* end month/year */
73 | .entry-end-container {
74 | display: flex;
75 | flex-direction: column;
76 | justify-content: end;
77 | align-items: center;
78 | grid-column: 1;
79 | grid-row: 2;
80 | min-height: 100px;
81 | }
82 |
83 | .entry-end-container span {
84 | position: relative;
85 | bottom: 25px;
86 | left: 1px;
87 | display: block;
88 | font-family: Arial, Helvetica, sans-serif;
89 | color: rgb(120,120,120);
90 | }
91 |
92 | .entry-end-month {
93 | font-size: 18px;
94 | }
95 |
96 | .entry-end-year {
97 | font-size: 11px;
98 | }
99 |
100 | /* end day */
101 | .entry-end-day {
102 | display: flex;
103 | flex-direction: column;
104 | justify-content: end;
105 | align-items: center;
106 | grid-column: 2;
107 | grid-row: 2;
108 | font-weight: bold;
109 | color: rgb(44,44,44);
110 | }
111 |
112 | .entry-end-day span {
113 | position: relative;
114 | bottom: 23px;
115 | display: block;
116 | font-family: Arial, Helvetica, sans-serif;
117 | font-size: 28px;
118 | }
119 |
120 | /* row 1 */
121 | .entry-header {
122 | grid-column: 3;
123 | grid-row: 1;
124 | font-family: Arial, Helvetica, sans-serif;
125 | color: rgb(82,79,77);
126 | margin-left: 10px;
127 | margin-right: 20px;
128 | }
129 |
130 | .entry-header h1 {
131 | margin-top: 10px;
132 | font-size: 22px;
133 | color: rgb(54,54,54)
134 | }
135 |
136 | .entry-header h2 {
137 | margin-bottom: 5px;
138 | font-size: 20px;
139 | color: rgb(64,64,64)
140 | }
141 |
142 | .entry-header h3 {
143 | margin-top: 5px;
144 | margin-bottom: 5px;
145 | margin-right: 10px;
146 | font-size: 16px;
147 | margin-left: 15px;
148 | color: rgb(74,74,74);
149 | display: inline;
150 | }
151 |
152 | .entry-header svg {
153 | position: relative;
154 | top: 2px;
155 | }
156 |
157 | /* row 2 */
158 | .entry-content {
159 | grid-column: 3;
160 | grid-row: 2;
161 | font-family: Arial, Helvetica, sans-serif;
162 | color: rgb(82,79,77);
163 | }
164 |
165 | .entry-content p {
166 | margin-top: 10px;
167 | color: rgb(44,44,44);
168 | margin-right: 20px;
169 | margin-left: 10px;
170 | }
171 |
172 | .entry-content small {
173 | font-size: 10px;
174 | display: block;
175 | text-align: end;
176 | margin: 5px;
177 | color: rgb(104,104,104)
178 | }
179 |
180 | /* entry buttons */
181 | .entry-buttons {
182 | grid-row: 3;
183 | grid-row: 1 / 5;
184 | background-color: #e8e0d5;
185 | border: none;
186 | transition: background-color 0.3s;
187 | cursor: pointer;
188 | }
189 |
190 | .entry-buttons:hover {
191 | background-color: #e7d7c1;
192 | }
193 |
194 | .entry-buttons:hover svg {
195 | transition: fill 0.3s;
196 | }
197 |
198 | .entry-buttons:hover svg {
199 | fill: rgb(44,44,44);
200 | }
201 |
202 | .entry-edit {
203 | grid-column: 4;
204 | }
205 |
206 | .entry-delete {
207 | grid-column: 5;
208 | }
209 |
210 | /* confirm delete prompt */
211 | .prompt-bg {
212 | position: fixed;
213 | top: 0px;
214 | left: 0px;
215 | width: 100%;
216 | height: 100%;
217 | justify-content: center;
218 | align-items: center;
219 | background-color: rgba(0,0,0,0.8);
220 | }
221 |
222 | .prompt {
223 | display: flex;
224 | flex-direction: column;
225 | align-items: end;
226 | background-color: rgb(244,242,237);
227 | border-radius: 5px;
228 | padding: 10px;
229 | }
230 |
231 | .cancel {
232 | position: relative;
233 | right: 25px;
234 | top: 10px;
235 | fill: rgb(175,175,175);
236 | transition: fill 0.2s;
237 | }
238 |
239 | .cancel:hover {
240 | fill: rgb(100,100,100);
241 | cursor: pointer;
242 | }
243 |
244 | .delete-buttons {
245 | display: flex;
246 | }
247 |
248 | .delete-buttons button {
249 | padding: 7px 15px;
250 | margin: 5px 5px;
251 | background-color: transparent;
252 | font-size: 16px;
253 | border: 1px solid black;
254 | border-radius: 5px;
255 | cursor: pointer;
256 | transition: background-color 0.3s;
257 | }
258 |
259 | .delete-buttons button:hover {
260 | background-color: #e8e0d5;
261 | }
262 |
263 | .prompt p {
264 | font-family: Arial, Helvetica, sans-serif;
265 | font-size: 16px;
266 | }
--------------------------------------------------------------------------------
/client/stylesheets/form-styles.css:
--------------------------------------------------------------------------------
1 | /* general styles */
2 | label {
3 | margin-right: 10px;
4 | font-size: 16px;
5 | color: rgb(41,41,41);
6 | }
7 |
8 | .form-container {
9 | width: 900px;
10 | margin: 0px auto;
11 | padding: 0px 15px 0px 15px;
12 | background-color: #e8e0d5;
13 | font-family: Arial, Helvetica, sans-serif;
14 | overflow: hidden;
15 | transition: max-height 0.3s ease-out;
16 | }
17 |
18 | .form-row {
19 | margin-top: 10px;
20 | padding: 1px;
21 | }
22 |
23 | .form-row-rating {
24 | margin-left: 27px;
25 | }
26 |
27 | .form-input {
28 | padding: 7px;
29 | font-size: 16px;
30 | border-radius: 5px;
31 | background-color: rgba(253,253,252);
32 | color: rgb(41,41,41);
33 | border: none;
34 | box-shadow: 0px 0px 2px rgb(114,108,99);
35 | margin-right: 10px;
36 | }
37 |
38 | .form-input:focus {
39 | outline: none;
40 | }
41 |
42 | .small-gap {
43 | height: 10px;
44 | }
45 |
46 | .big-gap {
47 | height: 25px;
48 | }
49 |
50 | /* specific styles */
51 |
52 | .form-location {
53 | width: 300px;
54 | }
55 |
56 | .form-region {
57 | width: 200px;
58 | }
59 |
60 | .form-country {
61 | width: 200px;
62 | }
63 |
64 | .form-activity {
65 | width: 150px;
66 | }
67 |
68 | .form-type {
69 | width: 150px;
70 | }
71 |
72 | .form-route {
73 | width: 300px;
74 | }
75 |
76 | .form-difficulty {
77 | width: 125px;
78 | }
79 |
80 | .form-rating {
81 | width: 125px;
82 | }
83 |
84 | .form-start-date {
85 | width: 150px;
86 | font-family: Arial, Helvetica, sans-serif;
87 | }
88 |
89 | .form-end-date {
90 | width: 150px;
91 | font-family: Arial, Helvetica, sans-serif;
92 | }
93 |
94 | .form-note {
95 | margin-left: 1px;
96 | width: 868px;
97 | height: 150px;
98 | resize: none;
99 | margin-right: 0px;
100 | padding: 15px;
101 | font-family: Arial, Helvetica, sans-serif;
102 | font-size: 16px;
103 | }
104 |
105 | .form-submit-container {
106 | display: flex;
107 | justify-content: end;
108 | }
109 |
110 | .form-submit {
111 | padding: 5px 15px 5px 10px;
112 | background-color: rgb(31, 125, 63);
113 | color: rgb(244,242,237);
114 | font-size: 18px;
115 | font-weight: bold;
116 | border: none;
117 | border-radius: 5px;
118 | box-shadow: 0px 0px 1px rgb(44,44,44);
119 | transition: background-color 0.3s;
120 | margin-bottom: 15px;
121 | margin-top: 10px;
122 | }
123 |
124 | .form-submit img {
125 | position: relative;
126 | top: 2px;
127 | padding-right: 5px;
128 | rotate: 0deg;
129 | translate: 0px;
130 | transition: rotate 0.3s, translate 0.3s;
131 | }
132 |
133 | .form-submit:hover img {
134 | cursor: pointer;
135 | background-color: rgb(31,100,63);
136 | rotate: 0.5turn;
137 | translate: -5px;
138 | }
139 |
140 | .form-submit:hover {
141 | cursor: pointer;
142 | background-color: rgb(31,100,63);
143 | }
144 |
145 | .route-row {
146 | margin-left: 25px;
147 | }
148 |
149 | .route-row path {
150 | pointer-events: none;
151 | }
152 |
153 | .form-star {
154 | position: relative;
155 | top: 7px;
156 | }
157 |
158 | .form-star-clear {
159 | color: rgb(125,125,125);
160 | margin-left: 10px;
161 | cursor: pointer;
162 | transition: color 0.2s;
163 | }
164 |
165 | .form-star-clear:hover {
166 | color: rgb(100,100,100);
167 | }
168 |
169 | .addRowButton {
170 | position: relative;
171 | top: 4px;
172 | margin-right: 7px;
173 | }
174 |
175 | /* .test {
176 | width: 100px;
177 | height: 100px;
178 | background-color: red;
179 | } */
--------------------------------------------------------------------------------
/client/stylesheets/header.css:
--------------------------------------------------------------------------------
1 | #header {
2 | background-color: rgb(41,41,41);
3 | box-shadow: 0px -40px 100px black;
4 | margin-bottom: 50px;
5 | }
6 |
7 | #header-container {
8 | display: flex;
9 | width: 900px;
10 | margin: auto;
11 | }
12 |
13 | #header-container h1 {
14 | padding-left: 2px;
15 | margin-bottom: 15px;
16 | margin-top: 15px;
17 | color: rgb(244,242,237);
18 | font-family:Arial, Helvetica, sans-serif;
19 | }
--------------------------------------------------------------------------------
/client/stylesheets/styles.css:
--------------------------------------------------------------------------------
1 | @import url('./header.css');
2 | @import url('./title.css');
3 | @import url('./form-styles.css');
4 | @import url('./entries.css');
5 |
6 | body {
7 | margin: 0px;
8 | background-color: rgb(244,242,237);
9 | }
10 |
11 | .footer {
12 | height: 400px;
13 | margin-top: 100px;
14 | background-color: rgb(213, 211, 206);
15 | }
--------------------------------------------------------------------------------
/client/stylesheets/title.css:
--------------------------------------------------------------------------------
1 | .title {
2 | display: flex;
3 | justify-content: space-between;
4 | font-family: Arial, Helvetica, sans-serif;
5 | width: 900px;
6 | margin: 0px auto 0px auto;
7 | background-color: rgb(84,84,84);
8 | border-radius: 5px 5px 0px 0px;
9 | padding: 15px 15px 0px 15px;
10 | transition: background-color 0.3s;
11 | box-shadow: 0px 0px 1px black;
12 | }
13 |
14 | .bottom-title {
15 | height: 20px;
16 | width: 930px;
17 | margin: 0px auto 50px auto;
18 | border-radius: 0px 0px 5px 5px;
19 | background-color: rgb(84,84,84);
20 | box-shadow: 0px 0px 1px black;
21 | transition: background-color 0.3s;
22 | }
23 |
24 | .title:hover {
25 | cursor: pointer;
26 | background-color: rgb(64,64,64);
27 | }
28 |
29 | .title:hover ~ .bottom-title {
30 | background-color: rgb(64,64,64);
31 | }
32 |
33 | .title h1 {
34 | margin: 0px;
35 | color: #f0e9e1;
36 | font-size: 26px;
37 | }
38 |
--------------------------------------------------------------------------------
/docs/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliendevlin/out-and-about/7d9de7564aa261bd40b90a2a874fc2766fc4dfc5/docs/schema.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "out-and-about",
3 | "version": "1.0.0",
4 | "description": "A mountain adventure journal",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production node server/server.js",
8 | "build": "cross-env NODE_ENV=production webpack",
9 | "dev": "cross-env NODE_ENV=development webpack serve & nodemon server/server.js"
10 | },
11 | "author": "Julien Devlin",
12 | "license": "ISC",
13 | "dependencies": {
14 | "express": "^4.18.2",
15 | "pg": "^8.9.0",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.20.12",
21 | "@babel/preset-env": "^7.20.2",
22 | "@babel/preset-react": "^7.18.6",
23 | "babel-loader": "^9.1.2",
24 | "cross-env": "^7.0.3",
25 | "html-webpack-plugin": "^5.5.0",
26 | "nodemon": "^2.0.20",
27 | "webpack": "^5.75.0",
28 | "webpack-cli": "^5.0.1",
29 | "webpack-dev-server": "^4.11.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Out & About
2 | ### A journaling app for ski bums, dirt bags, mountaineers, or all of the above.
3 | #### Future Work
4 | * submit several locations/routes as part of one entry
5 | * image uploads
6 | * auto complete for existing entries
7 | * 3rd party API to populate db with routes/trails/etc
8 | * authentication
9 | * Maps/stats tabs
10 | * pagination
11 | * 3rd party integrations (lighterpack)
12 | * user feeds
13 | * expanded user-facing error handling
14 | #### React Component Layout
15 | |-- Index
16 | |-- App
17 | |-- Form
18 | |-- Entries Container
19 | |-- Entry Items
20 | |-- Update Prompt
21 | |-- Delete Prompt
22 | #### Server Layout
23 | |-- server
24 | |-- entries
25 | |-- GET
26 | |-- POST
27 | |-- PUT
28 | |-- DELETE
29 | |-- form
30 | |-- GET
31 | #### Database Schema
32 | 
33 |
--------------------------------------------------------------------------------
/server/controllers/entry-controller.js:
--------------------------------------------------------------------------------
1 | const db = require('../models/model');
2 | const queries = require('../models/entry-queries')
3 |
4 | const entryController = {}
5 |
6 | // Get entry info from all columns in database, ordered by entry start date
7 | entryController.getEntries = (req, res, next) => {
8 | // Check if entry id stored in locals, if so add a where condition to query for that entry, if not query for all entries
9 | let selectQuery = queries.selectAll;
10 | const queryParams = [];
11 |
12 | if (Object.hasOwn(res.locals, 'entryId')) {
13 | selectQuery += 'WHERE entry_routes.entry_id = ($1) ';
14 | queryParams.push(res.locals.entryId);
15 | }
16 |
17 | selectQuery += 'ORDER BY entries.start_date DESC';
18 |
19 | // Query for all columns in all tables and save to locals
20 | db.query(selectQuery, queryParams)
21 | .then(data => {
22 | res.locals.entries = data.rows;
23 | return next();
24 | })
25 | .catch(err => {
26 | return next({
27 | log: `ERROR - entryController.getEntries: ${err}`,
28 | message: {err: 'Failed to retrieve entries from database. Check server log for details.'}
29 | });
30 | });
31 | }
32 |
33 | entryController.setEntry = (req, res, next) => {
34 | // Initialize variables for storage during transaction
35 | let regionId;
36 | let countryId;
37 | let locationId;
38 | let routeId;
39 | let entryId;
40 |
41 | // Destrcture request body properties to use for transaction
42 | const { country, region, location, route, typeId, difficultyId, note, startDate, endDate, rating } = req.body;
43 |
44 | // Begin transaction
45 | db.query('BEGIN')
46 | // Search for country, insert if not found
47 | .then(() => db.query(queries.selectCountry, [country]))
48 | .then(countryQuery => {
49 | if (countryQuery.rows[0]) return countryQuery;
50 | return db.query(queries.insertCountry, [country]);
51 | })
52 | // Search for region, insert if not found
53 | .then(countryQuery => {
54 | countryId = countryQuery.rows[0]._id;
55 | return db.query(queries.selectRegion, [region]);
56 | })
57 | .then(regionQuery => {
58 | if (regionQuery.rows[0]) return regionQuery;
59 | return db.query(queries.insertRegion, [region]);
60 | })
61 | // Search for location, insert if not found
62 | .then(regionQuery => {
63 | regionId = regionQuery.rows[0]._id;
64 | return db.query(queries.selectLocation, [location, regionId, countryId]);
65 | })
66 | .then(locationQuery => {
67 | if (locationQuery.rows[0]) return locationQuery;
68 | return db.query(queries.insertLocation, [location, regionId, countryId]);
69 | })
70 | // Search for route, insert if not found
71 | .then(locationQuery => {
72 | locationId = locationQuery.rows[0]._id;
73 | return db.query(queries.selectRoute, [route, typeId, difficultyId, locationId]);
74 | })
75 | .then(routeQuery => {
76 | if (routeQuery.rows[0]) return routeQuery;
77 | return db.query(queries.insertRoute, [route, typeId, difficultyId, locationId]);
78 | })
79 | // Insert entry
80 | .then(routeQuery => {
81 | routeId = routeQuery.rows[0]._id;
82 | return db.query(queries.insertEntry, [note, startDate, endDate, 1]);
83 | })
84 | // Insert entry route
85 | .then(entryQuery => {
86 | entryId = entryQuery.rows[0]._id;
87 | return db.query(queries.insertEntryRoute, [entryId, routeId, rating]);
88 | })
89 | // Commit the transaction
90 | .then(() => {
91 | return db.query('COMMIT')
92 | })
93 | // Store the entry id
94 | .then(() => {
95 | res.locals.entryId = entryId;
96 | return next();
97 | })
98 | // Catch any errors from the transaction
99 | .catch(err => {
100 | db.query('ROLLBACK');
101 |
102 | return next({
103 | log: `ERROR - entryController.setEntries: ${err}`,
104 | message: {err: 'Failed to create entry in database. Check server log for details.'}
105 | });
106 | });
107 |
108 | }
109 |
110 | // Deletes an entry from database
111 | entryController.deleteEntry = (req, res, next) => {
112 | // Destructure entry ID for entry to delete
113 | const { entryId } = req.body;
114 |
115 | // Delete database records of the entry from entry_routes and the entry record from entries
116 | db.query('BEGIN')
117 | .then(() => db.query(queries.removeEntryRoutes, [entryId]))
118 | .then(() => db.query(queries.removeEntry, [entryId]))
119 | .then(() => db.query('COMMIT'))
120 | .then(() => next())
121 | .catch((err) => {
122 | db.query('ROLLBACK');
123 |
124 | return next({
125 | log: `ERROR - entryController.deleteEntries: ${err}`,
126 | message: {err: 'Failed to delete entry in database. Check server log for details.'}
127 | });
128 | });
129 | }
130 |
131 | module.exports = entryController;
132 |
--------------------------------------------------------------------------------
/server/controllers/form-controller.js:
--------------------------------------------------------------------------------
1 | const db = require('../models/model');
2 | const queries = require('../models/form-queries');
3 |
4 | const formController = {}
5 |
6 | // Queries database for form dropdown options
7 | formController.getFormOptions = (req, res, next) => {
8 | db.query(queries.selectActivities)
9 | .then(activities => {
10 | res.locals.formOptions = {};
11 | res.locals.formOptions.activities = activities.rows;
12 |
13 | return db.query(queries.selectTypes);
14 | })
15 | .then(types => {
16 | res.locals.formOptions.types = types.rows;
17 |
18 | return db.query(queries.selectDifficulties);
19 | })
20 | .then(difficulties => {
21 | res.locals.formOptions.difficulties = difficulties.rows;
22 |
23 | return next();
24 | })
25 | .catch(err => {
26 | next({
27 | log: `ERROR - formController.getFormOptions: ${err}`,
28 | message: {err: 'Failed to retrieve form options from database. Check server log for details.'}
29 | });
30 | });
31 | }
32 |
33 | module.exports = formController
--------------------------------------------------------------------------------
/server/models/entry-queries.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // select all from all tables, ordered by entry PK
3 | selectAll: `
4 | SELECT
5 | entry_routes._id AS entry_routes_id,
6 | entry_routes.entry_id,
7 | entries.start_date,
8 | entries.end_date,
9 | types.activity_id,
10 | activities.activity,
11 | routes.type_id,
12 | types.type,
13 | types.convention,
14 | entry_routes.route_id,
15 | routes.route,
16 | routes.difficulty_id,
17 | difficulties.difficulty,
18 | routes.location_id,
19 | locations.location,
20 | locations.region_id,
21 | regions.region,
22 | locations.country_id,
23 | countries.country,
24 | entry_routes.rating,
25 | entries.note,
26 | entries.post_date
27 |
28 | FROM entry_routes
29 | INNER JOIN entries ON entries._id = entry_routes.entry_id
30 | INNER JOIN routes ON routes._id = entry_routes.route_id
31 | INNER JOIN types ON types._id = routes.type_id
32 | INNER JOIN activities ON activities._id = types.activity_id
33 | INNER JOIN locations ON locations._id = routes.location_id
34 | INNER JOIN regions ON regions._id = locations.region_id
35 | INNER JOIN countries ON countries._id = locations.country_id
36 | INNER JOIN difficulties ON difficulties._id = routes.difficulty_id
37 | `,
38 | // select a country
39 | selectCountry: `
40 | SELECT *
41 | FROM countries
42 |
43 | WHERE country = ($1)
44 | `,
45 | // select a region
46 | selectRegion: `
47 | SELECT *
48 | FROM regions
49 |
50 | WHERE region = ($1)
51 | `,
52 | // select a location
53 | selectLocation: `
54 | SELECT *
55 | FROM locations
56 |
57 | WHERE location = ($1)
58 | AND region_id = ($2)
59 | AND country_id = ($3)
60 | `,
61 | // select a route
62 | selectRoute: `
63 | SELECT *
64 | FROM routes
65 |
66 | WHERE route = ($1)
67 | AND type_id = ($2)
68 | AND difficulty_id = ($3)
69 | AND location_id = ($4)
70 | `,
71 | // insert a country
72 | insertCountry: `
73 | INSERT INTO countries (country)
74 | VALUES ($1)
75 |
76 | RETURNING _id
77 | `,
78 | // insert a region
79 | insertRegion: `
80 | INSERT INTO regions (region)
81 | VALUES ($1)
82 |
83 | RETURNING _id
84 | `,
85 | // insert a location
86 | insertLocation: `
87 | INSERT INTO locations (location, region_id, country_id)
88 | VALUES ($1, $2, $3)
89 |
90 | RETURNING _id
91 | `,
92 | // insert a route
93 | insertRoute: `
94 | INSERT INTO routes (route, type_id, difficulty_id, location_id)
95 | VALUES ($1, $2, $3, $4)
96 |
97 | RETURNING _id
98 | `,
99 | // insert an entry
100 | insertEntry: `
101 | INSERT INTO entries (note, start_date, end_date, user_id)
102 | VALUES ($1, $2, $3, $4)
103 |
104 | RETURNING _id
105 | `,
106 | // insert an entry route
107 | insertEntryRoute: `
108 | INSERT INTO entry_routes (entry_id, route_id, rating)
109 | VALUES ($1, $2, $3)
110 | `,
111 | // remove entry routes
112 | removeEntryRoutes:`
113 | DELETE FROM entry_routes
114 | WHERE entry_routes.entry_id = ($1)
115 | `,
116 | // remove entry
117 | removeEntry:`
118 | DELETE FROM entries
119 | WHERE entries._id = ($1)
120 | `
121 | }
--------------------------------------------------------------------------------
/server/models/form-queries.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | selectActivities:`
3 | SELECT *
4 | FROM activities
5 | `,
6 |
7 | selectTypes:`
8 | SELECT *
9 | FROM types
10 | `,
11 |
12 | selectDifficulties:`
13 | SELECT *
14 | FROM difficulties
15 | `
16 | }
--------------------------------------------------------------------------------
/server/models/model.js:
--------------------------------------------------------------------------------
1 | // connect to db and export pool
2 | const { Pool } = require('pg');
3 |
4 | // const PG_URI = [insert postgresql uri]
5 |
6 | const pool = new Pool({
7 | connectionString: PG_URI
8 | });
9 |
10 | module.exports = {
11 | query: (text, params, callback) => {
12 | console.log('--QUERY EXECUTED--\n', text, '\n');
13 | return pool.query(text, params, callback);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 |
4 | const app = express();
5 | const PORT = 3000;
6 |
7 | const entryController = require('./controllers/entry-controller');
8 | const formController = require('./controllers/form-controller');
9 |
10 | // parse all request bodies
11 | app.use(express.json());
12 |
13 | // Get all entries route
14 | app.get('/entries',
15 | entryController.getEntries,
16 | (req, res) => {
17 | return res.status(200).json(res.locals.entries);
18 | });
19 |
20 | // Create new entry route
21 | app.post('/entries',
22 | entryController.setEntry,
23 | entryController.getEntries,
24 | (req, res) => {
25 | return res.status(201).json(res.locals.entries);
26 | });
27 |
28 | // Update entry route
29 | app.put('/entries',
30 | entryController.deleteEntry,
31 | entryController.setEntry,
32 | entryController.getEntries,
33 | (req, res) => {
34 | return res.status(200).json(res.locals.entries);
35 | });
36 |
37 | // Delete entry route
38 | app.delete('/entries',
39 | entryController.deleteEntry,
40 | (req, res) => {
41 | return res.sendStatus(200);
42 | });
43 |
44 | // Gets form values route
45 | app.get('/form',
46 | formController.getFormOptions,
47 | (req, res) => {
48 | return res.status(200).json(res.locals.formOptions)
49 | })
50 |
51 | // Serve app and static files for production environments
52 | if (process.env.NODE_ENV === 'production') {
53 | app.use('/client', express.static(path.resolve(__dirname, '..', 'client')));
54 | app.get('/', (req, res) => res.status(200).sendFile(path.resolve(__dirname, '..', 'client', 'index.html')));
55 | }
56 |
57 | // Catch-all route
58 | app.use ('/', (req,res, next) =>{
59 | return next({
60 | log: 'Express catch all handler caught unknown route',
61 | status: 404,
62 | message: {err: 'Route not found'}
63 | });
64 | });
65 |
66 | // Default global error object & global error handler
67 | const defaultErr = {
68 | log: 'Express error handler caught an unknown middleware error',
69 | status: 400,
70 | message: { err: 'An error occurred' },
71 | };
72 |
73 | app.use((err, req, res, next) => {
74 | const errorObj = Object.assign(defaultErr, err);
75 | console.log(errorObj.log);
76 | return res.status(errorObj.status).json(errorObj.message);
77 | });
78 |
79 | // Start server
80 | app.listen(PORT, () => {
81 | console.log(`Server listening on port: ${PORT}`);
82 | });
83 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: process.env.NODE_ENV,
6 | entry: {
7 | src: path.join(__dirname, 'client', 'index.js')
8 | },
9 | output: {
10 | filename: 'bundle.js',
11 | path: path.join(__dirname, 'client', 'build')
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.jsx?/,
17 | exclude: /node_modules/,
18 | use: {
19 | loader: 'babel-loader',
20 | options: {
21 | presets: ['@babel/preset-env', '@babel/preset-react']
22 | }
23 | }
24 | },
25 | ]
26 | },
27 | plugins: [
28 | new HtmlWebpackPlugin({
29 | template: path.join(__dirname, 'client', 'index.html')
30 | })
31 | ],
32 | devServer: {
33 | static: {
34 | directory: path.resolve(__dirname, 'client')
35 | },
36 | proxy: {
37 | '/entries': 'http://localhost:3000',
38 | '/form': 'http://localhost:3000'
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------