Skip to content

Commit 8a85f36

Browse files
authored
Merge pull request #70 from hwgilbert16/develop
v1.0.9 Release
2 parents bf3eef6 + a3e5410 commit 8a85f36

24 files changed

+211
-112
lines changed

Dockerfile

+15-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
# syntax=docker/dockerfile:1.3-labs
2+
FROM node:lts-alpine3.18 as builder
3+
4+
WORKDIR /usr/src/app
5+
6+
RUN apk add g++ make py3-pip
7+
8+
COPY package*.json .
9+
RUN npm clean-install --production --silent --legacy-peer-deps
10+
11+
COPY . .
12+
RUN npm run generate
13+
RUN npm run build
14+
215
FROM node:lts-alpine3.18
316

417
ARG NODE_ENV
@@ -19,16 +32,9 @@ ARG SCHOLARSOME_RECAPTCHA_SECRET
1932

2033
WORKDIR /usr/src/app
2134

22-
COPY package*.json .
23-
24-
RUN apk add g++ make py3-pip
25-
RUN npm install --production --silent --unsafe-perm --legacy-peer-deps
26-
RUN apk del g++ make py3-pip
27-
2835
COPY . .
29-
30-
RUN npm run generate
31-
RUN npm run build
36+
COPY --from=builder /usr/src/app/dist ./dist
37+
COPY --from=builder /usr/src/app/node_modules ./node_modules
3238

3339
RUN <<EOL
3440
touch .env
@@ -37,7 +43,6 @@ RUN <<EOL
3743
echo "DATABASE_URL=$DATABASE_URL\n" >> .env
3844
echo "JWT_SECRET=$JWT_SECRET\n" >> .env
3945
echo "HTTP_PORT=$HTTP_PORT\n" >> .env
40-
4146
echo "S3_STORAGE_ENDPOINT=$S3_STORAGE_ENDPOINT\n" >> .env
4247
echo "S3_STORAGE_ACCESS_KEY=$S3_STORAGE_ACCESS_KEY\n" >> .env
4348
echo "S3_STORAGE_SECRET_KEY=$S3_STORAGE_SECRET_KEY\n" >> .env

README.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ https://scholarsome.com
1919

2020
</div>
2121

22+
<p align="center">
23+
<a href="https://news.ycombinator.com/item?id=36454783" target="_blank"><img height=53 src="https://hackerbadge.now.sh/api?id=36454783&type=orange" alt="Featured on HackerNews"></a>
24+
</p>
25+
2226
<p align="center">
2327
<img src="https://i.imgur.com/MshTOaL.png">
2428
</p>
@@ -27,7 +31,9 @@ https://scholarsome.com
2731

2832
Scholarsome <a href="http://ipa-reader.xyz/?text=%CB%88sk%C3%A4l%C9%99rs(%C9%99)m%2F">(pronounced ˈskälərs(ə)m/)</a> is a web-based and open source studying system. Through the use of flashcards, among other core features, users can practice memorization of terms and definitions, along with keeping their data secure locally.
2933

30-
While other services have begun to paywall core functionalities, Scholarsome intends to offer an equal alternative that does not compromise on feature sets.
34+
While other services have begun to paywall core functionalities, Scholarsome intends to offer an equal alternative that does not compromise on features.
35+
36+
You can read more about our design philosophy <a href="https://github.com/hwgilbert16/scholarsome#design-philosophy">here.</a>
3137

3238
## Features
3339

@@ -76,6 +82,18 @@ Scholarsome is an open source project. We believe in a transparent development p
7682
- **Bug reports.** If you run into an issue using Scholarsome, please <a href="https://github.com/hwgilbert16/scholarsome/issues/new">create a bug report</a>. Make sure you attach the `bug` label to your issue.
7783
- **Feature requests.** We'd love to hear your ideas for future features. Please <a href="https://github.com/hwgilbert16/scholarsome/issues/new">create an issue</a> and attach the `feature request` label to propose a new feature.
7884

85+
## Design Philosophy
86+
87+
Many existing flashcard systems exist that offer countless features - Anki, SuperMemo, etc. However, we recognize that many of these tools can be daunting to new users, or are bloated with features that can be seen as overwhelming. This encourages new flashcard users to stick to simpler tools, causing them to lose out on more advanced functionality. Additionally, not all of these tools are open source.
88+
89+
Scholarsome will bridge this gap between ease of use and functionality in multiple ways.
90+
91+
One way is by offering a familiar web-based interface. There is no syncing between multiple devices to worry about, as data is stored, accessed, and edited from a central server. The process of syncing and having to think about where data is stored can be a confusing topic. Having the place where flashcards are edited be the same place they are stored simplifies this process.
92+
93+
We're also selective about the order in which features are being added. Instead of using a scattershot method, we're working our way up from the simplest features that are the most commonly utilized to the most complex so that they can build upon each other. We write easy to understand guides that are intended to be understood from first glance by the layman. From the ground up, Scholarsome has been designed to have powerful learning tools, but still be understandable by anybody.
94+
95+
It's important to note that Scholarsome is far from complete. We're firm believers of shipping fast and early to gain feedback from users. While some features may be absent at the moment, we love having early users that can ensure we have a stable foundation through valuable feedback.
96+
7997
## Contact
8098

8199
For formal inquiries, you can contact us via [email protected]

apps/api/src/app/users/users.controller.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,8 @@ export class UsersController {
7878
delete user.verified;
7979
delete user.email;
8080

81-
for (let i = 0; i < user.sets.length; i++) {
82-
if (cookies) {
83-
if (user.sets[i].private && user.sets[i].authorId !== cookies.id) {
84-
user.sets = user.sets.splice(i, i + 1);
85-
}
86-
} else {
87-
user.sets = user.sets.filter((set) => set.private === false);
88-
}
81+
if (!cookies || user.id !== cookies.id) {
82+
user.sets = user.sets.filter((set) => set.private === false);
8983
}
9084

9185
return {

apps/api/src/main.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ async function bootstrap() {
2626

2727
const server = express();
2828
const app = await NestFactory.create(AppModule, new ExpressAdapter(server), {
29-
bufferLogs: true
29+
bufferLogs: process.env.NODE_ENV === "production"
3030
});
3131
const logger = LoggerFactory("Scholarsome");
3232
app.useLogger(logger);
33-
logger.log("Application Started!");
3433

3534
app.useGlobalPipes(
3635
new ValidationPipe({
@@ -68,11 +67,12 @@ async function bootstrap() {
6867

6968
const document = SwaggerModule.createDocument(app, config);
7069
fs.writeFileSync("./dist/api-spec.json", JSON.stringify(document));
71-
SwaggerModule.setup("api", app, document);
7270

7371
await app.init();
7472

7573
http.createServer(server).listen(process.env.HTTP_PORT);
74+
75+
logger.log("Scholarsome has started!");
7676
}
7777

7878
bootstrap();

apps/docs/docs/installation/installing.md

+18-5
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,27 @@ mkdir ~/scholarsome && cd ~/scholarsome
3333
Download the compose file.
3434

3535
```
36-
wget https://raw.githubusercontent.com/hwgilbert16/scholarsome/develop/compose.yml
36+
wget https://raw.githubusercontent.com/hwgilbert16/scholarsome/release/compose.yml
3737
```
3838

39+
:::caution
40+
If you will be using a valid SSL certificate with Scholarsome, you will need to modify your `compose.yml` file to inform Docker Compose what port you would like the SSL version of Scholarsome to be accessible on. If you use an SSL certificate, Scholarsome will run both an HTTP and HTTPS version of the site.
41+
42+
Open the `compose.yml` file in any text editor and navigate to line 29.
43+
44+
```
45+
# - "(your ssl port):8443"
46+
```
47+
48+
Remove the hashtag to uncomment the line, and replace `(your ssl port)` with the port that you would like the SSL version of Scholarsome to be accessible on.
49+
50+
Keep in mind that **non-root** users, by default, **do not have permission to bind to ports lower than 1024.** If you will be running SSL on a port lower than 1024, ensure that the user running the Scholarsome process has necessary permissions.
51+
:::
52+
3953
Download the environment file and make a copy of it.
4054

4155
```
42-
wget https://raw.githubusercontent.com/hwgilbert16/scholarsome/develop/.env.compose.example && cp .env.compose.example .env
56+
wget https://raw.githubusercontent.com/hwgilbert16/scholarsome/release/.env.compose.example && cp .env.compose.example .env
4357
```
4458

4559
Open the `.env` file in any text editor.
@@ -49,7 +63,7 @@ Expand the dropdown below, it lists Scholarsome's environment variables. These a
4963
Additionally, if you are using S3 as your storage medium, you will need to fill the `S3_` fields as well.
5064

5165
:::info
52-
If the SMTP fields are left blank, users will be verified by default. Most installations do not need to enforce email verification.
66+
If the SMTP fields are left blank, users will not have to verify their emails. Most installations do not need to enforce email verification, unless you are planning to expose Scholarsome to other users.
5367
:::
5468

5569
<details>
@@ -80,7 +94,6 @@ If the SMTP fields are left blank, users will be verified by default. Most insta
8094

8195
</details>
8296

83-
8497
Start the service in a detached state.
8598

8699
```
@@ -111,7 +124,7 @@ docker pull hwgilbert16/scholarsome
111124
Download the environment file and make a copy of it.
112125

113126
```
114-
wget https://raw.githubusercontent.com/hwgilbert16/scholarsome/develop/.env.docker.example && cp .env.docker.example .env
127+
wget https://raw.githubusercontent.com/hwgilbert16/scholarsome/release/.env.docker.example && cp .env.docker.example .env
115128
```
116129

117130
Open `.env` in any text editor.

apps/front/src/app/app.module.ts

+23
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,29 @@ Quill.register("modules/imageResize", ImageResize);
6868
QuillConfigModule.forRoot({
6969
modules: {
7070
imageResize: true,
71+
keyboard: {
72+
bindings: {
73+
tab: {
74+
key: 9,
75+
handler: () => {
76+
const editors = document.querySelectorAll("quill-editor");
77+
78+
const buttons = [
79+
...Array.from(editors[0].querySelectorAll("button")),
80+
...Array.from(editors[0].querySelectorAll("[role=\"button\"]")),
81+
...Array.from(editors[1].querySelectorAll("button")),
82+
...Array.from(editors[1].querySelectorAll("[role=\"button\"]"))
83+
];
84+
85+
for (const button of buttons) {
86+
button.setAttribute("tabindex", "-1");
87+
}
88+
89+
return true;
90+
}
91+
}
92+
}
93+
},
7194
toolbar: [
7295
["bold", "italic", "underline", "strike"],
7396
["code-block"],

apps/front/src/app/create/study-set/create-study-set.component.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Component, ComponentRef, ElementRef, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
2-
import { HttpClient } from "@angular/common/http";
32
import { AlertComponent } from "../../shared/alert/alert.component";
43
import { Router } from "@angular/router";
54
import { SetsService } from "../../shared/http/sets.service";
@@ -13,11 +12,7 @@ import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
1312
styleUrls: ["./create-study-set.component.scss"]
1413
})
1514
export class CreateStudySetComponent implements OnInit {
16-
/**
17-
* @ignore
18-
*/
1915
constructor(
20-
private readonly http: HttpClient,
2116
private readonly router: Router,
2217
private readonly sets: SetsService,
2318
private readonly titleService: Title,
@@ -35,6 +30,8 @@ export class CreateStudySetComponent implements OnInit {
3530

3631
protected formDisabled = false;
3732

33+
protected emptyTitleAlert = false;
34+
3835
protected faQuestionCircle = faQuestionCircle;
3936

4037
// index starts at 0
@@ -43,16 +40,19 @@ export class CreateStudySetComponent implements OnInit {
4340
async createSet() {
4441
const cards: { index: number; term: string; definition: string; }[] = [];
4542

46-
if (!this.titleInput.element.nativeElement.value) {
43+
if (!this.titleInput.element.nativeElement.value && !this.emptyTitleAlert) {
4744
const alert = this.titleInput.createComponent<AlertComponent>(AlertComponent);
4845

4946
alert.instance.message = "Title must not be empty";
5047
alert.instance.type = "danger";
5148
alert.instance.dismiss = true;
5249
alert.instance.spacingClass = "mt-3";
5350

51+
this.emptyTitleAlert = true;
52+
setTimeout(() => this.emptyTitleAlert = false, 3000);
53+
5454
return;
55-
}
55+
} else if (!this.titleInput.element.nativeElement.value) return;
5656

5757
for (const card of this.cards) {
5858
if (card.component.instance.term.length !== 0 && card.component.instance.definition.length !== 0) {

apps/front/src/app/header/header.component.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ export class HeaderComponent implements OnInit, AfterViewInit, OnDestroy {
101101
}
102102

103103
async ngOnInit(): Promise<void> {
104+
if (this.cookieService.get("authenticated")) {
105+
// we set this.user here so that it can be checked on every router event and log users out if auth invalid
106+
// however since header initializes on the homepage, this.user will not be set immediately after login
107+
// technically this would cause an issue if the tokens were invalid immediately after login
108+
// however it is more than overwhelmingly likely that any token issues with be on a future page reload when the user is already logged in
109+
this.signedIn = true;
110+
const user = await this.usersService.myUser();
111+
112+
if (user) {
113+
this.user = user;
114+
} else {
115+
this.signedIn = false;
116+
}
117+
}
118+
104119
this.sharedService
105120
.isUpdateAvailable()
106121
.then((r) => (this.updateAvailable = r));
@@ -112,9 +127,15 @@ export class HeaderComponent implements OnInit, AfterViewInit, OnDestroy {
112127

113128
if (!this.hidden && this.signedIn) {
114129
const user = await this.usersService.myUser();
115-
if (user) this.user = user;
116130

117-
await this.viewAvatar();
131+
// if this user was authenticated and is now no longer authenticated, sign them out
132+
if (this.user && !user) {
133+
await this.authService.logout();
134+
await this.router.navigate([""]);
135+
} else if (user) {
136+
this.user = user;
137+
}
138+
118139
this.profilePictureModal.updateAvatarEvent.subscribe(async () => await this.viewAvatar());
119140
}
120141
}
@@ -139,13 +160,11 @@ export class HeaderComponent implements OnInit, AfterViewInit, OnDestroy {
139160

140161
this.checkIfVerifiedInCookie();
141162

142-
if (this.deviceService.isTablet() || this.deviceService.isMobile()) {
163+
if (this.deviceService.isMobile()) {
143164
this.isMobile = true;
144165
}
145166

146-
if (this.cookieService.get("authenticated")) {
147-
this.signedIn = true;
148-
}
167+
if (this.signedIn && !this.hidden) await this.viewAvatar();
149168

150169
// Hide modals when the route changes
151170
this.router.events.subscribe(() => this.modalRef?.hide());

apps/front/src/app/homepage/homepage.component.html

+16-24
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,26 @@
88
<div class="d-flex header">
99
<h2>Your sets</h2>
1010
</div>
11-
<div class="row row-cols-1 row-cols-md-4 g-4 py-4 px-3 w-100">
12-
<div *ngIf="sets && sets.length === 0" class="text-secondary">
11+
<div class="row row-cols-1 row-cols-md-4 g-4 py-4 px-3 w-100" *ngIf="user">
12+
<div *ngIf="user.sets.length === 0" class="text-secondary">
1313
You haven't created any sets yet.
1414
<a [routerLink]="['/create/set']">Create your first one.</a>
1515
</div>
16-
<a
17-
*ngFor="let set of sets"
18-
[routerLink]="['/study-set', set.id]"
19-
class="text-decoration-none"
20-
>
21-
<div class="col">
22-
<div class="card shadow set-card" role="button">
23-
<div class="card-body">
24-
<h5 class="card-title">
25-
{{ set.title }}
26-
<fa-icon
27-
*ngIf="set.private"
28-
[icon]="['far', 'eye-slash']"
29-
></fa-icon>
30-
</h5>
31-
<h6 class="card-subtitle text-muted">
32-
{{ set.cards.length }} term{{
33-
this.set.cards.length > 1 ? 's' : ''
34-
}}
35-
</h6>
36-
</div>
16+
<div class="col" *ngFor="let set of user.sets">
17+
<div class="card shadow-sm set-card h-100" [routerLink]="['/study-set', set.id]" role="button">
18+
<div class="card-body d-flex flex-column">
19+
<h5 class="card-title">
20+
{{ set.title }}
21+
<fa-icon
22+
*ngIf="set.private"
23+
[icon]="['far', 'eye-slash']"
24+
></fa-icon>
25+
</h5>
26+
</div>
27+
<div class="card-footer mt-auto">
28+
<small class="text-body-secondary">Updated on {{ set.updatedAt.toLocaleString('en-us', { month:'short', day: 'numeric', year:'numeric'}) }}</small>
3729
</div>
3830
</div>
39-
</a>
31+
</div>
4032
</div>
4133
</div>

0 commit comments

Comments
 (0)