Skip to content

Commit dd24423

Browse files
authored
Merge pull request #19 from hwgilbert16/develop
v1.0.3 Release - Anki imports + media support
2 parents 03a84a0 + 0e6811c commit dd24423

File tree

86 files changed

+28383
-22690
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+28383
-22690
lines changed

.env.compose.example

+11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ JWT_SECRET=
1111
# Port to expose Scholarsome on
1212
HTTP_PORT=
1313

14+
# Data storage configuration
15+
# If local, file storage will be managed by Docker Compose
16+
STORAGE_TYPE=
17+
18+
# Required if storage type is s3
19+
S3_STORAGE_ENDPOINT=
20+
S3_STORAGE_ACCESS_KEY=
21+
S3_STORAGE_SECRET_KEY=
22+
S3_STORAGE_REGION=
23+
S3_STORAGE_BUCKET=
24+
1425

1526
# ---
1627
# Everything past this line is optional

.env.docker.example

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ REDIS_PORT=
1717
REDIS_USERNAME=
1818
REDIS_PASSWORD=
1919

20+
# Data storage configuration
21+
STORAGE_TYPE=
22+
23+
# Required if storage type is local
24+
# Absolute filepath
25+
STORAGE_LOCAL_DIR=
26+
27+
# Required if storage type is s3
28+
S3_STORAGE_ENDPOINT=
29+
S3_STORAGE_ACCESS_KEY=
30+
S3_STORAGE_SECRET_KEY=
31+
S3_STORAGE_REGION=
32+
S3_STORAGE_BUCKET=
33+
2034

2135
# ---
2236
# Everything past this line is optional

.env.example

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ REDIS_PORT=
2222
REDIS_USERNAME=
2323
REDIS_PASSWORD=
2424

25+
# Data storage configuration
26+
STORAGE_TYPE=
27+
28+
# Required if storage type is local
29+
# Absolute filepath
30+
STORAGE_LOCAL_DIR=
31+
32+
# Required if storage type is s3
33+
S3_STORAGE_ENDPOINT=
34+
S3_STORAGE_ACCESS_KEY=
35+
S3_STORAGE_SECRET_KEY=
36+
S3_STORAGE_REGION=
37+
S3_STORAGE_BUCKET=
38+
2539

2640
# ---
2741
# Everything past this line is optional

Dockerfile

+29-20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
# syntax=docker/dockerfile:1.3-labs
12
FROM node:18
23

34
ARG NODE_ENV
45
ARG DATABASE_PASSWORD
56
ARG DATABASE_URL
67
ARG JWT_SECRET
8+
ARG HTTP_PORT
79

810
ARG SMTP_HOST
911
ARG SMTP_PORT
@@ -26,25 +28,32 @@ COPY . .
2628
RUN npm run generate
2729
RUN npm run build
2830

29-
RUN touch .env
30-
31-
RUN echo "NODE_ENV=$NODE_ENV\n" >> .env
32-
RUN echo "DATABASE_PASSWORD=$DATABASE_PASSWORD\n" >> .env
33-
RUN echo "DATABASE_URL=$DATABASE_URL\n" >> .env
34-
RUN echo "JWT_SECRET=$JWT_SECRET\n" >> .env
35-
36-
RUN echo "SMTP_HOST=$SMTP_HOST\n" >> .env
37-
RUN echo "SMTP_PORT=$SMTP_PORT\n" >> .env
38-
RUN echo "SMTP_USERNAME=$SMTP_USERNAME\n" >> .env
39-
RUN echo "SMTP_PASSWORD=$SMTP_PASSWORD\n" >> .env
40-
RUN echo "HOST=$HOST\n" >> .env
41-
RUN echo "SSL_KEY_BASE64=$SSL_KEY_BASE64\n" >> .env
42-
RUN echo "SSL_CERT_BASE64=$SSL_CERT_BASE64\n" >> .env
43-
RUN echo "SCHOLARSOME_RECAPTCHA_SITE=$SCHOLARSOME_RECAPTCHA_SITE\n" >> .env
44-
RUN echo "SCHOLARSOME_RECAPTCHA_SECRET=$SCHOLARSOME_RECAPTCHA_SECRET\n" >> .env
45-
RUN echo "REDIS_HOST=$REDIS_HOST" >> .env
46-
RUN echo "REDIS_PORT=$REDIS_PORT" >> .env
47-
RUN echo "REDIS_USERNAME=$REDIS_USERNAME" >> .env
48-
RUN echo "REDIS_PASSWORD=$REDIS_PASSWORD" >> .env
31+
RUN <<EOL
32+
touch .env
33+
echo "NODE_ENV=$NODE_ENV\n" >> .env
34+
echo "DATABASE_PASSWORD=$DATABASE_PASSWORD\n" >> .env
35+
echo "DATABASE_URL=$DATABASE_URL\n" >> .env
36+
echo "JWT_SECRET=$JWT_SECRET\n" >> .env
37+
echo "HTTP_PORT=$HTTP_PORT\n" >> .env
38+
39+
echo "S3_STORAGE_ENDPOINT=$S3_STORAGE_ENDPOINT\n" >> .env
40+
echo "S3_STORAGE_ACCESS_KEY=$S3_STORAGE_ACCESS_KEY\n" >> .env
41+
echo "S3_STORAGE_SECRET_KEY=$S3_STORAGE_SECRET_KEY\n" >> .env
42+
echo "S3_STORAGE_REGION=$S3_STORAGE_REGION\n" >> .env
43+
echo "S3_STORAGE_BUCKET=$S3_STORAGE_BUCKET\n" >> .env
44+
echo "SMTP_HOST=$SMTP_HOST\n" >> .env
45+
echo "SMTP_PORT=$SMTP_PORT\n" >> .env
46+
echo "SMTP_USERNAME=$SMTP_USERNAME\n" >> .env
47+
echo "SMTP_PASSWORD=$SMTP_PASSWORD\n" >> .env
48+
echo "HOST=$HOST\n" >> .env
49+
echo "SSL_KEY_BASE64=$SSL_KEY_BASE64\n" >> .env
50+
echo "SSL_CERT_BASE64=$SSL_CERT_BASE64\n" >> .env
51+
echo "SCHOLARSOME_RECAPTCHA_SITE=$SCHOLARSOME_RECAPTCHA_SITE\n" >> .env
52+
echo "SCHOLARSOME_RECAPTCHA_SECRET=$SCHOLARSOME_RECAPTCHA_SECRET\n" >> .env
53+
echo "REDIS_HOST=$REDIS_HOST" >> .env
54+
echo "REDIS_PORT=$REDIS_PORT" >> .env
55+
echo "REDIS_USERNAME=$REDIS_USERNAME" >> .env
56+
echo "REDIS_PASSWORD=$REDIS_PASSWORD" >> .env
57+
EOL
4958

5059
CMD [ "npm", "run", "serve:node" ]

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## <p align="center"><img src="https://raw.githubusercontent.com/hwgilbert16/scholarsome/develop/apps/front/src/assets/scholarsome-logo-purple-lowercase.svg" height="50%" width="50%"></p>
1+
## <p align="center"><img src="https://raw.githubusercontent.com/hwgilbert16/scholarsome/develop/apps/front/src/assets/header/scholarsome-logo-purple-lowercase.svg" height="50%" width="50%"></p>
22

33
<div align="center">
44

@@ -8,6 +8,8 @@
88

99
https://scholarsome.com
1010

11+
<a href="https://discord.gg/hRgVvc5MKf">![](https://img.shields.io/badge/-Join%20our%20Discord-white?style=flat&logo=Discord&logoColor=blue)</a>
12+
1113
<a href="">![](https://img.shields.io/github/license/hwgilbert16/scholarsome?style=flat-square&color=blue)</a>
1214
<a href="">![](https://img.shields.io/badge/contributions-welcome-orange?style=flat-square)</a>
1315
<a href="">![](https://img.shields.io/github/issues/hwgilbert16/scholarsome?style=flat-square)</a>
@@ -20,7 +22,7 @@ https://scholarsome.com
2022

2123
## Introduction
2224

23-
Scholarsome is an 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.
25+
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 an 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.
2426

2527
While other services have begun to paywall core functionalities, Scholarsome intends to offer an equal alternative that does not compromise on feature sets.
2628

@@ -29,9 +31,9 @@ While other services have begun to paywall core functionalities, Scholarsome int
2931
Implemented features include:
3032

3133
- Create your own study sets ✅
34+
- Import sets from Anki and Quizlet ✅
3235
- Study flashcards in either traditional or progressive mode ✅
3336
- Use quizzes to test yourself ✅
34-
- Import created sets from Quizlet ✅
3537
- Edit your sets on the fly ✅
3638
- Make sets private if studying with others ✅
3739

apps/api/.eslintrc.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
},
99
{
1010
"files": ["*.ts", "*.tsx"],
11-
"rules": {}
11+
"rules": {
12+
"no-control-regex": "off"
13+
}
1214
},
1315
{
1416
"files": ["*.js", "*.jsx"],

apps/api/src/app/app.module.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { RedisModule } from "@liaoliaots/nestjs-redis";
1818
import { JwtModule } from "@nestjs/jwt";
1919
import { APP_INTERCEPTOR } from "@nestjs/core";
2020
import { GlobalInterceptor } from "./auth/global.interceptor";
21+
import { MediaModule } from "./media/media.module";
2122

2223
@Module({
2324
imports: [
@@ -26,7 +27,8 @@ import { GlobalInterceptor } from "./auth/global.interceptor";
2627
serveStaticOptions: {
2728
cacheControl: true,
2829
maxAge: 31536000
29-
}
30+
},
31+
exclude: ["/api/(.*)"]
3032
}),
3133
ConfigModule.forRoot({
3234
isGlobal: true
@@ -48,6 +50,7 @@ import { GlobalInterceptor } from "./auth/global.interceptor";
4850
MailModule,
4951
CardsModule,
5052
UsersModule,
53+
MediaModule,
5154
{
5255
...JwtModule.registerAsync({
5356
useFactory: (configService: ConfigService) => ({

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

+80-1
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,35 @@ export class CardsController {
7979
@UseGuards(AuthenticatedGuard, CreateCardGuard)
8080
@Post()
8181
async createCard(@Body() body: CreateCardDto): Promise<ApiResponse<Card>> {
82+
let media = [];
83+
84+
const termScanned = await this.cardsService.scanAndUploadMedia(body.term, body.setId);
85+
if (termScanned) {
86+
body.term = termScanned.scanned;
87+
media = [...termScanned.media];
88+
}
89+
90+
const definitionScanned = await this.cardsService.scanAndUploadMedia(body.definition, body.setId);
91+
if (definitionScanned) {
92+
body.definition = definitionScanned.scanned;
93+
media = [...media, ...definitionScanned.media];
94+
}
95+
8296
return {
8397
status: "success",
8498
data: await this.cardsService.createCard({
8599
index: body.index,
86100
term: body.term,
87101
definition: body.definition,
102+
media: {
103+
createMany: {
104+
data: media.map((c) => {
105+
return {
106+
name: c
107+
};
108+
})
109+
}
110+
},
88111
set: {
89112
connect: {
90113
id: body.setId
@@ -102,6 +125,53 @@ export class CardsController {
102125
@UseGuards(AuthenticatedGuard, UpdateCardGuard)
103126
@Put(":cardId")
104127
async updateCard(@Param() params: CardIdParam, @Body() body: UpdateCardDto): Promise<ApiResponse<Card>> {
128+
const card = await this.cardsService.card({ id: params.cardId });
129+
if (!card) throw new NotFoundException();
130+
131+
let media = [];
132+
133+
// there's likely a better way to write the media deletion checking
134+
// but for now the implementation here is sufficient
135+
let mediaChecked = false;
136+
137+
if (body.term) {
138+
const termScanned = await this.cardsService.scanAndUploadMedia(body.term, card.setId);
139+
if (termScanned) {
140+
body.term = termScanned.scanned;
141+
media = [...termScanned.media];
142+
}
143+
144+
for (const media of card.media) {
145+
mediaChecked = true;
146+
147+
if (!body.definition && !body.term.includes(media.name)) {
148+
await this.cardsService.deleteMedia(card.setId, media.name);
149+
} else if (
150+
body.definition &&
151+
!body.definition.includes(media.name) &&
152+
!body.term.includes(media.name)
153+
) {
154+
await this.cardsService.deleteMedia(card.setId, media.name);
155+
}
156+
}
157+
}
158+
159+
if (body.definition) {
160+
const definitionScanned = await this.cardsService.scanAndUploadMedia(body.definition, card.setId);
161+
if (definitionScanned) {
162+
body.definition = definitionScanned.scanned;
163+
media = [...media, ...definitionScanned.media];
164+
}
165+
166+
if (!mediaChecked) {
167+
for (const media of card.media) {
168+
if (!body.definition.includes(media.name)) {
169+
await this.cardsService.deleteMedia(card.setId, media.name);
170+
}
171+
}
172+
}
173+
}
174+
105175
return {
106176
status: "success",
107177
data: await this.cardsService.updateCard({
@@ -111,7 +181,16 @@ export class CardsController {
111181
data: {
112182
index: body.index,
113183
term: body.term,
114-
definition: body.definition
184+
definition: body.definition,
185+
media: {
186+
createMany: {
187+
data: media.map((c) => {
188+
return {
189+
name: c
190+
};
191+
})
192+
}
193+
}
115194
}
116195
})
117196
};

apps/api/src/app/cards/cards.module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Module } from "@nestjs/common";
1+
import { forwardRef, Module } from "@nestjs/common";
22
import { DatabaseModule } from "../providers/database/database.module";
33
import { CardsController } from "./cards.controller";
44
import { CardsService } from "./cards.service";
55
import { SetsModule } from "../sets/sets.module";
66
import { UsersModule } from "../users/users.module";
77

88
@Module({
9-
imports: [DatabaseModule, SetsModule, UsersModule],
9+
imports: [DatabaseModule, UsersModule, forwardRef(() => SetsModule)],
1010
controllers: [CardsController],
1111
providers: [CardsService],
1212
exports: [CardsService]

0 commit comments

Comments
 (0)