-
Notifications
You must be signed in to change notification settings - Fork 0
/
report.tex
2159 lines (1879 loc) · 94.9 KB
/
report.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% Editing? Enable word wrap in your editor.
\documentclass{report}
\usepackage{fullpage}
\usepackage{a4}
\usepackage{geometry}
\usepackage{fancyhdr}
\usepackage{etoolbox}
\usepackage{hyperref}
\usepackage{graphicx,float}
\usepackage{subfig}
\usepackage{array,ragged2e,pst-node,pst-dbicons}
\usepackage{mdframed}
\usepackage{minted}
\usepackage{tikz}
\usetikzlibrary{shapes, arrows}
\tikzstyle{terminator} = [rectangle, draw, text centered, rounded corners, minimum height=2em]
\tikzstyle{process} = [rectangle, draw, text centered, minimum height=2em]
\tikzstyle{decision} = [diamond, draw, text centered, minimum height=2em]
\tikzstyle{data}=[trapezium, draw, text centered, trapezium left angle=60, trapezium right angle=120, minimum height=2em]
\tikzstyle{connector} = [draw, -latex']
\usemintedstyle{monokai}
\BeforeBeginEnvironment{minted}{\begin{mdframed}[backgroundcolor=black!90]}
\AfterEndEnvironment{minted}{\end{mdframed}}
\usepackage[backend=biber,
style=numeric,
natbib=true,
sorting=none,
citestyle=authoryear]{biblatex}
\addbibresource{./references.bib}
% Required by OCR
\newcommand{\candidatename}{Rain}
\newcommand{\candidatenumber}{0000}
\newcommand{\centernumber}{000000}
\newcommand{\centername}{Center Name}
\newcommand{\qualification}{H446, 2021}
% Apply headers and footers to every page, including chapter pages
% Required by OCR
\pagestyle{fancy}
\setlength{\headheight}{12pt}
\addtolength{\topmargin}{-11pt}
\patchcmd{\chapter}{\thispagestyle{plain}}{\thispagestyle{fancy}}{}{}
\fancyhead[L]{\candidatename}
\fancyhead[C]{Candidate: \candidatenumber}
\fancyhead[R]{Center: \centernumber}
\fancyfoot[L]{\qualification}
\fancyfoot[C]{}
\fancyfoot[R]{\thepage}
\renewcommand{\familydefault}{\sfdefault} % serif fonts are an eyesore
\title{Intelligent flashcards}
\author {
\candidatename \\\\
Candidate: \candidatenumber \\
Center: \centernumber \\
\centername \\\\
\qualification
}
\begin{document}
\raggedbottom
\maketitle
\setcounter{page}{2}
\tableofcontents
\chapter{Analysis}
\section{The Problem}
\paragraph{}
Spaced repetition of bite-sized content has been proven to help commit content to memory. Intelligently spacing content apart further increases people's ability to recall information. Flashcards have long been known to be an effective method to memorise content, but actually using them is cumbersome. For one, having to carry a deck at all times is inconvenient; let alone having to constantly sort and move cards around as you become more and less familiar with the content inside. This is a shame, as managing cards intelligently helps to maximise efficiency in retaining information.
\paragraph{}
This problem is a good candidate for solving computationally: most people carry around a device capable of accessing the internet at all times (more than are willing to carry several decks of flashcards!). In addition, algorithms can help to categorise content and signal the optimal time to practice something. This project aims to solve both of these problems through an online web application.
\section{The Science}
\paragraph{}
By exploiting the psychological spacing effect, people can learn much more efficiently. Humans find it much easier to recall and apply content that has been spaced across several days and shuffled around, compared to content that has been crammed in a single session. Students that learned content across several days in multiple locations were able to recall up to an additional 4 items on average, compared to those who learned the same content on the same day in one location\footcite{ShufflingMathematicsProblems}.
\section{Existing Solutions}
A few pieces of software have already solved this problem.
\subsection{Tinycards}
\paragraph{}
Duolingo's Tinycards \footcite{Tinycards} (defunct since September 2020), was likely the closest implementation to what this project aims to achieve. The user could pick one of already created decks of cards containing material they wanted to study, or create their own. Then, the software would present a range of challenges based on how well the user retained the information.
\begin{figure}[h!]
\includegraphics[width=\linewidth]{./media/tinycards.png}
\label{fig:tinycards1}
\caption{A screenshot of the desktop user interface for Tinycards. Sourced from \href{https://web.archive.org/web/20211005121604/https://www.pcmag.com/reviews/tinycards-by-duolingo}{PCMag}.}
\end{figure}
\paragraph{}
This ranged from simply showing the card and letting the user get to grips with it; matching one side of the card with another card from a line-up; or typing the other side of the card from memory. The site eventually had to shut down due to lack of financial resources, but not before gaining over 1.5 million active monthly users\footcite{TinycardsShutdown}.
\subsection{Physical Flashcards}
\paragraph{}
Paper flashcards are likely most people's first thought when it comes to spaced-repetition learning. They are simple to understand, and \emph{can} be very effective when paired with a suitable system such as Leitner boxes.
\paragraph{}
Using them effectively, however, is a challenge. Organising many cards is very difficult — not to mention that cards can be damaged or lost. In addition, creating them can be very time-consuming, and many do not know how to use the cards properly to maximise recall once they have been created.
\subsection{Quizlet}
\paragraph{}
Quizlet solves some of these problems by featuring a flashcard mode. However, there is no algorithm to intelligently distribute and challenge cards, like there was in Duolingo's Tinycards. As such, this is little more than a digitised analogue of paper cards. While this is certainly better than having to carry about paper cards, this can absolutely be improved upon with intelligent algorithms.
\section{Stakeholders}
\subsubsection{Students}
\paragraph{}
The main target of this software are students — particularly those at secondary and A-Level. They are the end users, and will make up most interactions. They will use the site to actually learn the content, so it should be intuitive and must meet their needs; the site must effectively help them what they need to learn. Students can be conversed with during or after school hours on most days of the week.
\subsubsection{Teachers}
\paragraph{}
Teachers are also important to consult with, even if they might not directly use the software; being teachers, their job is to ensure that content is taught effectively. They have significant experience and training in effective learning methods, so they are a vital resource in ensuring the efficacy of the software. Time with teachers will be much more restricted, especially as the year progresses towards exam season.
\subsection{Students}
\paragraph{}
Having outlined the issues with existing solutions, interviews with several students were conducted to gather details about what they would need from this piece of software. Some notable excerpts are listed below. Names have been reduced to initials.
\hrulefill
% --------------------------------------
\subsubsection{How often do you revise?}
\begin{quote}{Student J:}
Never; well, not really. When I say "never", I mean right before like a test or something.
\end{quote}
\begin{quote}{Student U:}
Every two days.
\end{quote}
% --------------------------------------
\subsubsection{What methods do you use for revision?}
\begin{quote}{Student J:}
\href{https://senecalearning.com/en-GB/}{Seneca}; It's easy to use and requires little input. If I'm deciding what questions I'm revising, I might not know what's gonna be on the test. Seneca has the content that I need.
\end{quote}
\begin{quote}{Student U:}
Mainly reading textbooks and exam questions.
\end{quote}
% --------------------------------------
\subsubsection{What features would you look for in a revision tool?}
\begin{quote}{Student J:}
Ideally, it gives me the questions that I don't know the best. Covers all the topics, obviously. The questions aren't asking me to repeat what I've just read. I hate that [about Seneca].
\end{quote}
% --------------------------------------
\subsubsection{What keeps you from revising as much as you should / revising more often?}
\begin{quote}{Student J:}
We live in a golden age of television and gaming. [Revision is boring by comparison.]
\end{quote}
\begin{quote}{Student U:}
Boredom.
\end{quote}
% --------------------------------------
\hrulefill
\paragraph{}
Note that while Student U revises often, the way they revise doesn't help to practice the skills they need. Their revision is passive (i.e. reading from a textbook) instead of active (e.g. answering recall and practice questions). Meanwhile, Student J uses active revision methods, but has not formed a routine around it.
\paragraph{}
In summary, students want a tool that is engaging and fun. More importantly, it must have the content that they need and present the information realistically. It should also help to build habits to ensure effectiveness.
\section{Requirements}
\paragraph{}
Based on the thoughts of the stakeholders, the following requirements were drawn up.
\subsection{Authentication}
Being able to target individual users allows for personalised learning. This means that there has to be a way for each user to be uniquely identified.
\begin{itemize}
\item Must have a registration page:
\begin{itemize}
\item Take username and password.
\item Password must be typed twice to prevent mistakes, and be masked to prevent shoulder surfing.
\item Optionally allow the user to use 2FA.
\end{itemize}
\item Must have a sign-in page.
\end{itemize}
\subsection{Feel and branding}
The software should feel friendly, familiar, casual and playful. One of the most oft cited reasons for avoiding revision is that it is boring compared to other things that they could be doing.
\begin{itemize}
\item Interface should be simple, uncluttered and clean.
\begin{itemize}
\item Interface should be as simple as possible. Nothing should be redundant. Prefer icons to text.
\item Interface should put the content front and centre and minimise distractions.
\item Interface should feature a dark theme, which may be more comfortable in some lighting conditions.
\item Interface should be accessible to those who are colour-blind (1 in 12 men are colour-blind\footcite{Colourblindness}).
\end{itemize}
\item Branding should be casual and playful.
\end{itemize}
\subsection{Decks and Cards}
Cards are the focus of the app, and what the user will spend most time interacting with.
\begin{itemize}
\item Users can create their own cards and decks.
\begin{itemize}
\item Users edit the front and back of their cards.
\item Users can choose to share their decks with other users.
\item Users can tag their decks, e.g. by subject / topic.
\end{itemize}
\item Users can search a list of public decks.
\begin{itemize}
\item A fuzzy search feature searches decks, the cards inside the deck, and the tags of the cards.
\item Users can import other users' decks to practice or edit their own copy.
\end{itemize}
\item Users are shown and tested on their decks. Algorithms figure out how well a user knows a card, and might present the card in different ways depending on how familiar the user is.
\begin{itemize}
\item Presented with the card to read and nothing else.
\item Presented with a card and asked to pick the other side from a line-up of several cards.
\item Presented with a card and then asked to type out the other side from memory.
\begin{itemize}
\item The checking mechanism should be fuzzy and lenient to typos or alternative phrasing. This might be accomplished through an algorithm like Levenshtein edit distance.
\item The software should allow a user to mark a question that has been judged incorrect as being correct. For example, this might be because of alternative phrasing that throws off edit distances.
\end{itemize}
\end{itemize}
\item The user is offered some sort of incentive to come back every day to practice.
\begin{itemize}
\item This could be accomplished through things like daily streaks on decks, graphs of performance and a daily challenge.
\end{itemize}
\end{itemize}
\subsection{Moderation}
The site is public with anyone being able to submit public cards. There needs to be a way to ensure that public cards are appropriate.
\begin{itemize}
\item Decks marked as forkable (i.e. public) can be flagged by users for review by a moderator.
\begin{itemize}
\item The offending card can be deleted by a moderator.
\item The offending user can be banned by a moderator.
\end{itemize}
\end{itemize}
\section{Success criteria}
\subsection{Backend}
\paragraph{}
The backend should satisfy all the requirements listed above. Furthermore, it should be secure against common exploits such as SQL injection to prevent leaking or destruction of user data. The backend should be able to run on any server machine in a portable manner, allowing for easy deployment and self-hosting, should users choose to do so. The backend should be thoroughly tested, perhaps using an automated testing engine. We can generate code coverage reports from our testing engine to view the effectiveness of our tests.
\subsection{Frontend}
\paragraph{}
The frontend should be responsive to many viewport sizes, be accessible, load quickly on slow connections and stay performant even on lower end hardware, as might be typical on a student's device. We can generate scores for these metrics using Google's Lighthouse, a web auditing tool that reports on important metrics that affect perceived and actual performance, as well as highlighting any accessibility issues. Lighthouse was chosen as it is an industry standard testing tool, and the Google search engine considers Lighthouse scores when ranking pages in its search results. At a minimum, we should strive for a score of 90 or higher in every category.
\paragraph{}
We can also end-to-end test the user interface using both automated means to iron out any bugs, and with users to locate any UX issues. We can measure this by having in person tests with users of the target demographic (13-21 year olds) and seeing how they interact with the software, measuring time it takes to do certain tasks and asking them to describe their experience and any pain points with the software.
\chapter{Design}
\paragraph{}
The software can be broken down like so:
\begin{itemize}
\item User \begin{itemize}
\item Decks \begin{itemize}
\item Cards \begin{itemize}
\item Front
\item Back
\item Ability history
\item Metadata, e.g. owner, forkable.
\end{itemize}
\end{itemize}
\item Account info \begin{itemize}
\item Username
\item Password hash
\item Session tokens
\end{itemize}
\end{itemize}
\end{itemize}
\paragraph{}
Everything is tied to each user. The user can create their own decks and cards, and share them with other users. Each deck can be tagged with a subject or topic, allowing for easy searching if they choose to make these decks public.
\section{Paradigm and architecture}
\paragraph{}
A website allows the software to target the largest amount of devices with the least effort. However, this has the downside of requiring stable internet access, which is not always available.
\paragraph{}
Originally, an object-oriented approach was considered. However, this does not make a lot of sense for this project. To ensure maintainability, scalability, and open up the possibility for development of mobile apps in future, a RESTful design was chosen for the backend. Because of this, each request does not leave behind persistent state in the server memory (i.e. it is stateless). As such, constructing instances of classes for each request just to be destroyed at the end of the request is not a good idea. A functional approach supports this architecture much better.
\paragraph{}
The user interface should be divorced from backend logic, similar to a "JAM-stack". This allows it to be deployed to a CDN for low latency globally, reducing hosting costs, and once again keeping the door open for dedicated mobile applications.
\section{Database design}
\begin{figure}[h!]
\includegraphics[width=\linewidth]{./media/db_schema.png}
\label{fig:database1}
\caption{Database schema. \href{https://drawsql.app/--347/diagrams/iroase}{Click here} to see a scalable diagram.}
\end{figure}
\paragraph{}
Users have many decks, which can hold many cards. Originally, a NoSQL document-driven database like MongoDB was going to be used, but due to the common request of wanting to share decks, a relational SQL database will be more suitable. These complex relationships would need multiple transactions in a NoSQL database, while a relational database is much more suitable as it can easily deal with complex, interconnected data in a single transaction. A search system could be implemented on cards that users have marked as forkable to allow users to adopt a copy of other decks to their account. This search feature could index cards based on keywords and exam boards.
\paragraph{}
Storing authorization info allows users to be remembered by the site. Storing the password in hashed form allows the user to be protected in case of database breaches, and salting the hash further prevents the use of rainbow tables.
\paragraph{}
Reports need to be saved to keep the site safe from spam or harmful content. These reports can then be reviewed by a moderator.
\paragraph{}
CardEvent is used to track the user's ability to remember cards. This can then be used to calculate how well a user knows a card.
\paragraph{}
Notifications are stored in the database, so the user can be told about important events such as a card they have reported has being checked by a moderator.
\subsection{Data flow}
\paragraph{}
A user signs up through the frontend, which saves their username and a salted hash of their password into the Users table. A user can then create a deck, which creates a deck in the Decks table, with a reference to their deck in UserDecks. They can then create cards, which creates cards in the Cards table, with a reference to their deck in UserDecks. Cards can be created, with a reference to their deck in DeckCards. Once a user begins using their decks, the software can then calculate the ability of the user to remember each card and store it in the database based on a "confidence" score and a date. Storing this information allows for an algorithm similar to Leitner boxes to be used to serve cards at optimal intervals. This cycle of reviewing cards repeatedly makes up the core functionality of the software.
\paragraph{}
If a user wants to share their deck with other users, they can mark it as public. This allows it to be returned in search results, and for other users to adopt a copy to their account for them to then use similarly.
\section{Algorithms}
\subsection{Calculating pull rate}
\paragraph{}
We can represent this mathematically. A card that keeps being forgotten will have a lower 'difficulty' compared to a card that is consistently remembered.
\[d = \frac{f}{1 + n + f} \]
$d$ is difficulty, $f$ is times forgotten, and $n$ is the number of times the card has been reviewed.
This can be used to select the kind of challenge the user is presented with.
\paragraph{}
We have a model for difficulty, but humans do not remember things linearly, however\footcite{ForgettingCurve}. There is a sharp decline in retention just one day after learning it. This is because it has not been shifted to long term memory. Because of this, a linear equation alone can't be used to judge the ability of a user to remember a card -- a separate system is needed to determine how often to pull a card.
\paragraph{}
We can use tagging similar to Leitner boxes to model this. A higher box has a longer interval between reviews.
After each challenge, we ask the user how confident they are with the card (i.e. when they next want to see it -- mixed into the pool immediately, in a few days, or in a week.). This is then stored, and when the date passes the card can be randomly pulled along with other cards that need to be reviewed.
\subsection{Challenge types}
\paragraph{}
Now that we have a way to estimate the difficulty of a card, we can start to decide what kind of challenges to serve the user once the card has been pulled. These challenges consider the current difficulty of the card.
\subsubsection{Difficulty thresholds}
\paragraph{}
$0 \leq d < \frac{1}{2}$ is a low difficulty card, so we serve a challenge that asks them to type out the other side of the card from memory.
\paragraph{}
$\frac{1}{2} \leq d < 1$ is a high difficulty card, so we serve a "challenge" that simply asks the user to review the card and rate it. We update the confidence accordingly based on if the user finds the card easy, medium, or hard.
\paragraph{}
Using these thresholds, we can have a variety of challenge types that scale to the user's ability.
\begin{tikzpicture}[node distance = 3cm]
\node [terminator] (start) {\textbf{Start}};
\node [decision, below of=start, xshift=5cm] (any) {Any overdue?};
\node [process, below of=any, xshift=-6cm] (pull) {Randomly pull overdue card};
\node [decision, below of=pull] (difficulty) {Card difficulty};
\node [process, right of=difficulty, xshift=1.5cm] (show) {Show card};
\node [process, below of=difficulty] (pick) {Multiple choice};
\node [process, left of=difficulty, xshift=-1.5cm] (type) {Type card};
\node [data, below of=pick] (rate) {Rate confidence};
\node [process, below of=rate] (update) {Update next due date};
\node [terminator, below of=any, xshift=2cm] (end) {\textbf{End}};
\path [connector] (start) -| (any);
\path [connector] (any) -| node [anchor=south] {yes} (pull);
\path [connector] (pull) -- (difficulty);
\path [connector] (difficulty) -- node [anchor=south] {low} (type);
\path [connector] (difficulty) -- node [anchor=east] {medium} (pick);
\path [connector] (difficulty) -- node [anchor=south] {high} (show);
\path [connector] (type) |- (rate);
\path [connector] (pick) -- (rate);
\path [connector] (show) |- (rate);
\path [connector] (rate) -- (update);
\path [connector] (update) -| (any);
\path [connector] (any) -| node [anchor=south] {no} (end);
\end{tikzpicture}
\subsubsection{Sign up}
\paragraph{}
On sign up, users are asked to send a username they want alongside their password to the server. First, it must check that the username is valid (alphanumeric and within a certain length constraint). If it is valid, the server then checks the database to see if the username is already taken. If it isn't taken, we hash their password with a salt, create a new entry in the Users table with their username, hashed password, salt, the current time. We create a session token and then send the token to the client. If the username is invalid or has already been taken, we send an error back to the client.
\subsubsection{Login}
\paragraph{}
On sign up, users enter their username and password. This is sent to the server, which retrieves the hash and salt from the database. We hash the password with the salt and then compare the hash to the stored hash. If they match, the password is correct, and we can create a session to send to the client. If either the username or the password is incorrect, we send an error to the client stating that the username or password is incorrect.
\subsection{Sessions}
Session tokens are symmetrical on the client and the server. The session token should be sent in Authorization header on almost every request. If the token is invalid or missing, we return a 401 Unauthorized.
\section{Frontend}
\subsection{Styles, feel and branding}
\paragraph{}
The app should be intuitive, casual and approachable, but still have some personality. A quirky yet casual feeling style was chosen to help users feel like they are "at home". Mock-up UI screens were created with Figma, and some are in the figures below. See \href{https://www.figma.com/file/7vs7errfunZKlr1wT6j52Z/faded}{this Figma sketch} for scalable versions of all the screens.
\begin{figure}[H]
\centering
\subfloat{\includegraphics[height=4.2cm]{./media/ui/home.png}}
\subfloat{\includegraphics[height=4.2cm]{./media/ui/home_dark.png}}
\label{fig:home1}
\caption{The home screen. Text is intentionally lowercase to feel casual; as if the user were typing to friends. A warm colour scheme and a quirky font further makes the site feel less like a sterile textbook. UI components like the navigation bar and cards are as minimal as possible, with further context being revealed on hover. The most important component (daily review) is the largest and separated with a line to hint to the user that is the most important. A streak is shown to motivate the user to practice every day, similar to some social media apps using streaks to encourage daily activity. Dark theme is also available for comfortable reading in low light conditions.}
\end{figure}
\begin{figure}[H]
\centering
\subfloat{\includegraphics[height=4.2cm]{./media/ui/textbox.png}}
\subfloat{\includegraphics[height=4.2cm]{./media/ui/review.png}}
\label{fig:question1}
\caption{Example question screens. A legible, clean sans-serif font with "correct" casing is used on material that is being learned to maximise legibility. Almost nothing else is on-screen, keeping the user focused.}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[height=17.5cm]{./media/ui/deck.png}
\label{fig:editor1}
\caption{The deck editor. Settings are below the page fold, with a scroll hint telling the user to scroll for settings. They can mark their deck as public, optionally set the course that the deck is related to, or delete the deck. There is a wireframe of the exam paper next to the course box, to assist them in locating the course code. The input box should check to see if the code enters matches a course, and provide feedback.}
\end{figure}
\subsection{Usability}
\subsubsection{Keyboard input}
\paragraph{}
Everything should be accessible via the keyboard. For flipping cards, the user could hit the space bar. For rating cards or picking the correct card from a multiple choice selection, the user should use the number keys for the corresponding item. This makes the interface usable in a highly efficient way, while also being accessible to users who might be blind or lack the motor skills to use a mouse.
\subsubsection{Localisation}
\paragraph{}
While the software will likely be mainly used by those who speak English, we can widen the audience globally by having the interface be translated to many languages. Translators can submit translations by submitting a pull request for their language to the repository; unfortunately, I can't translate the interface myself to other languages.
\subsubsection{Accessibility}
All text in the interface is at a readable size (14px at minimum), and the contrast ratios for text with this colour scheme pass WCAG AA guidelines. The colours that have been chosen should not pose any confusion for people with colour blindness; this has been verified by putting the screens through a colour-blind simulating filter. Touch targets are at least 48px by 48px in size to make them easy to hit.
\section{Testing strategy}
\paragraph{}
A test-driven driven development approach is highly useful. We can designate behaviour that we expect, and then we can iterate solutions very quickly with confidence that the software works as expected. As such, a high quality test suite and plan is essential.
\subsection{TypeScript}
\paragraph{}
TypeScript significantly reduces the amount of tests that need to be written. The TypeScript compiler will detect type issues at compile time, unlike JavaScript, where you are silently left with undefined values at runtime. This means that we don't need to test how functions handle illegal inputs; we can use TypeScript's type system to do this for us, so long as we provide robust type definitions and attach interfaces (a way to define expected types in each field) to our objects.
\subsection{API}
\paragraph{}
Using Jest, we can write tests that specify the behaviour that we want and then expect certain values. We can mock calls to the database to ensure that we have a consistent database state for each test. Each function should be tested with legal inputs (TypeScript's compiler can handle illegal inputs), and each HTTP endpoint should also be simulated by the test library with a range of potential valid and invalid inputs, as would be expected in the real world.
\subsection{End-to-end testing}
\paragraph{}
At the end of development, we can emulate how a real user will interact with the app, from sign up to using cards, using a library such as Cypress to simulate a browser.
\paragraph{}
However, there will still be blind spots in the app that we may have forgotten about. As such, we should give the software to real people and let them use the site, and have them try to break it. There are also some things that need to be tested that simply can't be automated, such as UX. For this, real world usage is vital.
\subsection{Frontend}
\paragraph{}
We can use Google's Lighthouse to identify potential accessibility and performance issues. Lighthouse can flag any issues we need to rectify as well as provide metrics to improve on, such as page load time. We can also simulate a browser using Cypress to test if each component works as expected. However, this also needs to be tested manually by users as not every user action be anticipated.
\chapter{Backend}
\paragraph{}
To keep things clean, only the minimum to demonstrate the "idea" of the code is shown in this document. The source code for this project can be viewed \href{https://github.com/iroase-app}{on GitHub, inside the orginsation "iroase-app"} under the BSD-3-Clause licence.
\paragraph{}
Our stack for the backend is TypeScript, running inside Node, which stores data in a PostgreSQL database. An test-driven development approach is used, with our tests written using \href{https://jestjs.io}{Jest}.
\subsection{Database setup}
The following SQL commands set up our user and session tables in PostgreSQL as according to \hyperref[fig:database1]{our schema}.
\begin{minted}[breaklines, linenos]{sql}
CREATE TABLE IF NOT EXISTS users (
"user_id" INT GENERATED ALWAYS AS IDENTITY,
username varchar NOT NULL,
hashed_password varchar NOT NULL,
created timestamp NOT NULL,
is_moderator boolean NOT NULL DEFAULT false,
CONSTRAINT users_pk PRIMARY KEY ("user_id"),
CONSTRAINT users_un UNIQUE (username)
);
CREATE TABLE IF NOT EXISTS sessions (
"session_id" INT GENERATED ALWAYS AS IDENTITY,
"token" varchar NOT NULL,
device varchar NOT NULL,
created timestamp NOT NULL,
"user_id" INT NOT NULL,
CONSTRAINT sessions_pk PRIMARY KEY ("session_id"),
CONSTRAINT "user_id" FOREIGN KEY ("user_id") REFERENCES users("user_id") ON DELETE CASCADE
);
\end{minted}
\subsection{Registration}
\subsubsection{Test cases}
\begin{minted}[breaklines, linenos]{typescript}
// Excerpt from ./src/__test__/register.test.ts
import app from '../app';
describe('/register', () => {
it('accepts a username and a password and sends back token', async () => {
const res = await supertest(app)
.post('/register')
.send({
username: 'Fo0bar',
password: 'secure password',
});
expect(res.body).toHaveProperty('session');
expect(res.body.username).toEqual('foobar');
expect(res.statusCode).toEqual(201);
});
// The full test suite can be found in the repository in the listed file.
});
\end{minted}
\paragraph{}
The server should reject the request if either the "username" or "password" field is missing; the username contains illegal characters; or the username or password is too long or short. "supertest" is a library that makes it easy to emulate a request to an Express web server. The test suite emulates several requests to "/register", and then expects certain responses based on the input it gave the server.
\subsubsection{Implementation}
We can use this middleware to make sure the username and password are what we want.
\begin{minted}[breaklines, linenos]{typescript}
// Excerpt from ./src/api/_common/auth/validate.ts
import { Request, Response } from 'express';
/**
* Check if the password is valid
* @param username The username to check
* @returns `true` if the password is valid, otherwise an error message as `string`.
*/
function user(username: string): string | true {
if (!username.match(/^[A-Za-z0-9]*$/g)) return 'illegalCharacters';
if (username.length > 24) return 'usernameTooLong';
if (username.length < 3) return 'usernameTooShort';
return true;
}
/**
* Check if the password is valid
* @param password The password to check
* @returns `true` if the password is valid, otherwise an error message as `string`.
*/
function pass(password: string): string | true {
if (password.length > 128) return 'passwordTooLong';
if (password.length < 8) return 'passwordTooShort';
return true;
}
/**
* Express middleware to validate the username and password
* @param req The request object
* @param res The response object
* @param next Callback to run next
*/
export default function validate(req: Request, res: Response, next: Function) {
if (!(req.body.username.length && req.body.password.length)) res.status(400).send({ error: 'fieldMissing' });
else {
const u = user(req.body.username);
const p = pass(req.body.password);
if (u !== true) {
res.status(400).send({ error: u });
} else if (p !== true) {
res.status(400).send({ error: p });
} else { next(); }
}
}
\end{minted}
\paragraph{}
We've now checked that the input we get from client requests is what we expect it to be, and then reject any requests that aren't allowed. Next, we attempt to store the user in the database. If the database rejects the operation, we check to see if it was due to a username collision.
\begin{minted}[linenos, breaklines]{typescript}
// Excerpt from ./src/api/register/index.ts
register.post('/', validate, async (req, res) => {
const client = await db.connect();
try {
await client.query('BEGIN');
const user = await client.query(`
INSERT INTO users (username, hashed_password, created, is_moderator)
VALUES ($1, $2, $3, $4)
RETURNING user_id;
`,
[req.body.username, await argon2.hash(req.body.password), new Date(), false]);
const token = (await randomBytesP(64)).toString('hex');
await client.query(`
INSERT INTO sessions ("user_id", "token", device, created)
VALUES ($1, $2, $3, $4);
`,
[user.rows[0].user_id, token, `${req.useragent?.os}: ${req.useragent?.browser}`, new Date()]);
await client.query('COMMIT');
res.status(201).send({ session: token, username: req.body.username });
} catch (e) {
if (e instanceof DatabaseError && e.code === '23505') res.status(409).send({ error: 'usernameCollision' });
else {
res.status(500).send({ error: 'internalError' });
console.error(`DB failure!! \n${e}`);
}
await client.query('ROLLBACK');
} finally {
client.release();
}
});
\end{minted}
\paragraph{}
If so, we notify the client. Finally, for good requests, we send back a session token. We can verify this by running our test suite:
\begin{figure}[H]
\includegraphics[width=\linewidth]{./media/development/backend/tests/register/pass.png}
\label{fig:registerValid1}
\caption{The tests for our registration endpoint pass!}
\end{figure}
\subsection{Making our tests and app containerised}
\paragraph{}
Before we go any further, it's probably wise to make sure that our app and tests run portably. Right now, the test suite (and deployment!) relies on a brittle, manual setup of a database running on the local machine. We can automate our tests and deploys in a reproducible way using Docker. Our \href{https://github.com/iroase-app/server/blob/main/Dockerfile}{Dockerfile} sets out how we should build the Docker image for our server, and our \href{https://github.com/iroase-app/server/blob/main/docker-compose.test.yml}{docker-compose} files show how the image should be deployed alongside a database.
\begin{figure}[H]
\includegraphics[width=\linewidth]{./media/development/backend/tests/docker/pass.png}
\label{fig:tests2}
\caption{We can now run our tests in a continuous integration (CI) environment! Here, the tests are running in GitHub Actions against a clean database. Our software runs on any machine that has Docker installed.}
\end{figure}
\subsection{Login}
\paragraph{}
Now that users can sign up, we need to allow them to sign in by exchanging a username and password for a session token. We can re-use the validation middleware we wrote earlier. A test suite has been written for the login endpoint that simulates a correct request and a variety of invalid requests. The test suite can be viewed \href{https://github.com/iroase-app/server/tree/main/src/__test__/login.test.ts}{in the repository here}.
\begin{minted}[linenos, breaklines]{typescript}
// Excerpt from ./src/api/login/index.ts
login.post('/', validate, async (req, res) => {
const user = await db.query(
'SELECT user_id, hashed_password FROM users WHERE username = $1;',
[req.body.username],
);
if (!user.rows[0]) {
/*
* If the user doesn't exist, we don't want to leak information.
* We pretend that the user exists but the password is wrong.
* We hash a dummy string for consistent execution time.
*/
await argon2.hash('very real password');
return res.status(400).send({ error: 'wrongCredentials' });
}
const isValid = await argon2.verify(user.rows[0].hashed_password, req.body.password);
if (isValid) {
const token = (await randomBytesP(64)).toString('hex');
await db.query(`
INSERT INTO sessions ("user_id", "token", device, created)
VALUES ($1, $2, $3, $4);
`,
[user.rows[0].user_id, token, `${req.useragent?.os}: ${req.useragent?.browser}`, new Date()]);
return res.send({ session: token, username: req.body.username });
}
return res.status(400).send({ error: 'wrongCredentials' });
});
\end{minted}
If a user doesn't exist, we hash a dummy string to ensure that the operation takes the same amount of time to complete regardless of the user's existence, preventing timing attacks (checking how long it takes to execute a request to infer properties something).
\paragraph{}
We run our tests, and they pass, allowing our users to sign in.
\subsection{Session verification}
\paragraph{}
Now that we can allow users to create accounts and exchange credentials for sessions, we need to ensure that we can verify that a session is valid. Clients should attach their session token to every auth-required request they make in the 'Authorization' header. We can do this by writing middleware that checks the token against the database.
\begin{minted}[breaklines, linenos]{typescript}
// Excerpt from ./src/api/_common/verifySession.ts
/**
* Middleware to check if the user is logged in.
* @param req Express request object.
* @param res Express response object.
* @param next Express next callback.
*/
export default async function verifySession(req: Request, res: Response, next: Function) {
let token;
if (req.headers.authorization) {
token = req.headers.authorization?.split(' ')[1];
} else {
token = null;
}
if (!token) res.status(401).send({ error: 'noBearer' });
else {
const user = await db.query(`
SELECT users.user_id, username, "token", is_moderator
FROM users INNER JOIN sessions ON (users.user_id = sessions.user_id)
WHERE "token" = $1;
`,
[token]);
if (!user.rows[0]) res.status(401).send({ error: 'unauthorized' });
else {
req.user = {
id: user.rows[0].user_id,
isModerator: user.rows[0].is_moderator,
username: user.rows[0].username,
};
next();
}
}
}
\end{minted}
\paragraph{}
The lines 9-15 in the excerpt above exist to check if the request actually sends a token. When our test suite sent a request with missing credentials, and this check wasn't implemented, the server would crash!
\begin{minted}[breaklines]{typescript}
app.use(verifySession);
\end{minted}
We then attach this middleware to routes that require authentication. Authentication protected routes are under the '/app' path. Requests that are missing a session token or have an invalid token will be rejected, and good requests attach the user's ID, username and their role (i.e. moderator or not) to the request object for each endpoint handler to use.
\paragraph{}
We can create a meta endpoint that returns the user's username and role if they attach their session token. This can also serve as a test for our session-verification middleware.
\begin{minted}[breaklines]{typescript}
// Excerpt from ./src/__test__/session/get.test.ts
describe('return meta information about the user', () => {
it('should return the user meta information', async () => {
const res = await supertest(loader)
.get('/app/session')
.set('Authorization', 'Bearer testToken');
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
username: 'TestUser',
isModerator: false,
});
});
it('should reject if session is missing or invalid', async () => {
let res = await supertest(loader)
.get('/app/session')
.set('Authorization', 'Bearer invalidToken');
expect(res.status).toBe(401);
expect(res.body.error).toBe('unauthorized');
res = await supertest(loader)
.get('/app/session')
.set('Authorization', 'Bearer ');
expect(res.status).toBe(401);
expect(res.body.error).toBe('noBearer');
});
});
\end{minted}
The endpoint itself is trivial, as all related logic is abstracted away by the verifySession middleware.
\begin{minted}[breaklines]{typescript}
// Excerpt from ./src/api/app/session/get.ts
whoami.get('/', (req, res) => {
res.status(200).send({ username: req.user?.username, isModerator: req.user?.isModerator });
});
\end{minted}
\subsection{Logout}
\paragraph{}
We can now log out by deleting the session. Our test checks to see that the correct row is removed from the database.
\begin{minted}[linenos, breaklines]{typescript}
// Excerpt from ./src/__test__/session/delete.test.ts
describe('logging out', () => {
it('should delete a session', async () => {
const res = await supertest(loader)
.delete('/app/session')
.set('Authorization', 'Bearer testToken')
.send();
expect(res.status).toBe(204);
const sessions = await db.default.query(`
SELECT * FROM sessions WHERE "token" = $1;
`,
['testToken']);
expect(sessions.rowCount).toBe(0);
});
});
\end{minted}
Once again, our endpoint is simple as well. All the token checking logic is handled by our middleware.
\begin{minted}[linenos, breaklines]{typescript}
logout.delete('/', async (req, res) => {
/*
* We can trust the authorization header is valid
* because we already checked it in the middleware.
* See ./src/api/_common/authMiddleware/verifySession.ts
*/
const token = req.headers.authorization!.split(' ')[1];
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
res.status(204).send();
});
\end{minted}
\section{Other trivial endpoints}
\paragraph{}
For the sake of brevity, most trivial endpoints from this point on will be omitted, as the ideas demonstrated in them are really similar to the ones above. (Creating, reading, updating, deleting information to and from the database; and validating user inputs.) Should you wish to see the code and test cases for these, you can find them in the GitHub repo. The code is under the BSD-3 licence. Only the interesting algorithms from here onwards will have code snippets embedded.
\subsection{Deck creation, listing and deletion}
\subsubsection{Deck creation}
Creating a deck first makes sure that the user provides a valid name. Then, it writes the deck to the database in the 'deck' table. Finally, it writes the deck to the user's collection in the 'user decks' table.
\subsubsection{Listing decks}
We need to show the user the decks that they have, (e.g. to display on the home page). We get the user ID from their session token, and then fetch all their decks (as well as their metadata), and return them to the user.
\subsubsection{Updating decks}
Users need to be able to rename their decks, change the course, make them public or private, and change the tags. Users can send a PATCH request to update some or all of the metadata about a deck:
\begin{minted}[linenos, breaklines]{typescript}
// Excerpt from ./src/api/app/deck/patch.ts
updateDeck.patch('/:id', async (req, res) => {
if (!req.body.name && !req.body.course && !req.body.public) return res.status(400).send({ error: 'noData' });
const deck = await db.query(/* sql */ `
SELECT * FROM user_decks WHERE deck_id = $1;
`, [req.params.id]);
if (!deck.rows[0]) return res.status(404).send({ error: 'deckNotFound' });
if (deck.rows[0].user_id !== req.user!.id) return res.status(403).send({ error: 'notOwner' });
const client = await db.connect();
await client.query('BEGIN');
try {
// Update the fields if they are provided.
if (req.body.name) {
await client.query(/* sql */ `
UPDATE decks SET name = $1 WHERE deck_id = $2;
`, [req.body.name, req.params.id]);
}
if (req.body.course) {
await client.query(/* sql */ `
UPDATE decks SET course = $1 WHERE deck_id = $2;
`, [req.body.course, req.params.id]);
}
if (req.body.public) {
await client.query(/* sql */ `
UPDATE decks SET public = $1 WHERE deck_id = $2;
`, [req.body.public, req.params.id]);
}
await client.query('COMMIT');
return res.send({ success: true });
} catch (e) {
await client.query('ROLLBACK');
console.error(`Error updating deck: ${e}`);
return res.status(500).send({ error: 'databaseError' });
} finally {
client.release();
}
});
\end{minted}
\chapter{Frontend}
\paragraph{}
The frontend repository contains translations that were \emph{NOT} created by me. These translations are simply .json files inside src/locales. The English translation is my own; all others have been sent by other users. You can see the pull requests for
\href{https://github.com/iroase-app/frontend/pulls?q=is%3Apr+label%3Alocalization+}{these translations here}.
\paragraph{}
For the frontend, we're going to use SvelteKit. Svelte components are compiled to plain HTML and JavaScript, and doesn't ship any runtime. This means that our frontend app will have tiny bundle sizes, and will be able to run even on low performance devices. SvelteKit allows us to server-side render our pages, increasing perceived performance and metrics like First Contentful Paint. In addition, SvelteKit also provides routing, allowing for smooth transitions between pages without the need for a full page refresh.
\paragraph{}
We're also going to use TailwindCSS, which is an atomic CSS framework that provides many utility classes. We can set our colour scheme and fonts in the Tailwind config file.
\subsection{Cross browser compatibility}
\paragraph{}
A wide range of modern browsers are supported. Inside the .browserslistrc file, we set conditions for browsers that we want to support.
\begin{minted}[breaklines]{yaml}
last 5 chrome versions
last 5 firefox versions
last 5 iOS major versions
last 5 edge versions
last 5 safari versions
last 5 opera versions
Firefox ESR
> 0.5% and not dead
> 2% in US
> 2% in alt-EU
> 2% in alt-AS
> 2% in alt-OC
not OperaMini all
not IE 11
not IE 10
not IE 9
not IE 8
\end{minted}
This is read by the Vite compiler used in SvelteKit, and the PostCSS autoprefixer plugin to add vendor prefixes to our CSS.
\subsection{Logo}
\paragraph{}
Right off the bat, we have a problem to deal with. The logo word mark is in Japanese. Normally for a word mark, we would simply download a font from a CDN. However, CJK fonts are very large as they span thousands of glyphs. This makes downloading a CJK font pretty silly for the majority of our users who will only ever need to see the logo in Japanese. We can solve this by converting the glyphs to an SVG. This cuts down the amount of data we need to send to the client significantly.
\subsection{Localisation}
\paragraph{}
It'd be nice to have a way to localise our app. However, we need to set things up from the start to not have to deal with time-consuming refactors later.
\subsubsection{RTL}
\paragraph{}
RTL support is needed if we are to target languages that are written from right to left, such as Arabic or Hebrew. The Tailwind plugin \href{https://github.com/20lives/tailwindcss-rtl}{tailwindcss-rtl} provides direction agnostic utilities that change the direction of the page based on the flow direction of the script.
\subsubsection{Language selection}
We can use the package \href{https://github.com/kaisermann/svelte-i18n}{svelte-i18n} to manage string dictionaries. Translations are created by updating the strings that correspond to each key in the dictionary.
\subsubsection{Implementation}
\paragraph{}
Now, we can register locales. For now, there is just en.json, for English. We run the helper function in our site's entrypoint to register all the locales. Now, we can easily use the dictionaries in our components, like so:
\begin{minted}[]{typescript}
{$_("landingPage.hero")}
\end{minted}
When the page loads, it replaces the reference with the correct string.
\paragraph{}
We can now use the i18-a11y extension for Visual Studio Code to show us previews of our translated strings in our components. If we reference a key that doesn't exist, we'll get a warning in our editor.
\begin{figure}[H]
\includegraphics[width=\linewidth]{./media/development/frontend/localisation/no_ext.png}
\includegraphics[width=\linewidth]{./media/development/frontend/localisation/with_ext.png}
\label{fig:localisation1}
\caption{We get friendly previews of our strings.}
\end{figure}
\subsubsection{Mirroring the interface}
\paragraph{}
While the tailwindcss-rtl library is very useful, the text-align utilities are currently broken. See \href{https://github.com/20lives/tailwindcss-rtl/issues/39}{this issue and pull request opened by me} for more info. Right now though, we can work around this by overwriting the broken utilities in our Tailwind.postcss file:
\begin{minted}[]{css}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.text-start {
text-align: start !important;
}
.text-end {
text-align: end !important;
}
}
\end{minted}
Neat! Now our interface is correctly mirrors based on the direction of the script, as seen in the images below.\footnote{Please forgive the machine translated Arabic.}
\begin{figure}[H]
\centering
\subfloat{\includegraphics[height=4.9cm]{./media/development/frontend/localisation/ltr.png}}
\subfloat{\includegraphics[height=4.9cm]{./media/development/frontend/localisation/rtl.png}}
\label{fig:localisation2}
\caption{The interface is mirrored if a language that uses an RTL script is used.}
\end{figure}
\section{Authentication}
\subsection{Register}
\paragraph{}
We can now create our register page. Using checks like the ones below, we can validate the user's input in real time. For the bad password list, we are using \href{https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10-million-password-list-top-1000.txt}{a public list of the 1,000 worst passwords.}\footnote{Be careful, the list contains offensive language and slurs.}
\begin{minted}[linenos, breaklines]{js}
{#if !username.match(/^[A-Za-z0-9]*$/g)}
<div
class="flex items-center"
transition:fly={{ duration: 300, y: 50 }}
>
<MdiWarning class="h-5 w-5 me-2" />
<span class="font-sans">
{$_("register.constraints.username.characters")}
</span>
</div>
{/if}
\end{minted}
\subsubsection{Sending the request}
\paragraph{}
Once the input has been validated, we can send the request to the server. First, we'll make a helper function to join the path with the location of the API server. This prevents hard-coding the location of our API server in many places across our codebase.
\begin{minted}[breaklines, linenos]{typescript}
export default function getURL(...path: string[]): string {
return [config.api.url, ...path].join('/');
}
\end{minted}
Of course, this function is also tested.
\begin{minted}[breaklines, linenos]{typescript}
describe("getURL", () => {
it("should return the correct url", () => {
let url = getURL("register", "foobar", "app", "123");
expect(url).toEqual(`${config.api.url}/register/foobar/app/123`);
url = getURL("hello", "world");
expect(url).toEqual(`${config.api.url}/hello/world`);
});
it("should return the root if no args are specified", () => {
const url = getURL();
expect(url).toEqual(`${config.api.url}`);
})
}
);
\end{minted}
\begin{figure}[H]
\centering
\includegraphics[height=8cm]{./media/development/frontend/tests/unit/getURL.png}
\label{fig:getURL1}
\caption{...and our tests pass.}
\end{figure}
\href{https://github.com/iroase-app/iroase/blob/main/media/ui/register/validation.mp4?raw=true}{https://github.com/iroase-app/iroase/blob/main/media/ui/register/validation.mp4?raw=true}
The video above shows the page in action, from validating the user input to sending the request, handling errors, and storing the token that has been sent. If the user is already logged in, we redirect them to the main app page. If the user tries to access the main app page while not logged in, we redirect them to the login page.
\subsection{Logging in}
\paragraph{}
Logging in works similarly to above. We just exchange the entered username and password for a token. See this video:
\href{https://raw.githubusercontent.com/iroase-app/iroase/main/media/ui/login/signin.mp4}{https://raw.githubusercontent.com/iroase-app/iroase/main/media/ui/login/signin.mp4}
\begin{minted}[breaklines, linenos]{typescript}
let username = "";
let password = "";
let fetching = false;
let error: false | string = false;
const submit = () => {
if (fetching === true) {
return;
}