Compare commits
46 Commits
feat/pw-re
...
front/play
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48724fd554 | ||
| bc13c10f1a | |||
| 91c9e2b295 | |||
| 585be2aa19 | |||
| 654022b48a | |||
| afab03baf8 | |||
| a52c10fc2c | |||
| f2ed598865 | |||
| 02fc8175f4 | |||
|
|
628e50a48d | ||
|
|
70ab56ce3a | ||
|
|
1fefe7912d | ||
|
|
46ef0a7f1b | ||
|
|
64640eda55 | ||
|
|
a6d9cb3b40 | ||
|
|
b61541f7b8 | ||
|
|
3ff523560b | ||
|
|
b61968706d | ||
|
|
2f27278d3a | ||
|
|
e1ab9fe118 | ||
|
|
b1d0415ba0 | ||
|
|
8ab85ab689 | ||
|
|
16cd794e3b | ||
|
|
f85c30a53b | ||
|
|
6da96ed886 | ||
|
|
852fbd5c87 | ||
|
|
7e866f9826 | ||
|
|
2f50f694f3 | ||
|
|
c9d3ef88e7 | ||
|
|
0ba3bec5aa | ||
|
|
539c35c903 | ||
|
|
e1463d41b9 | ||
|
|
01394056a6 | ||
|
|
1396fcb39c | ||
|
|
1255343b97 | ||
|
|
f7562c18bd | ||
|
|
bf09a25eb5 | ||
|
|
373128ba53 | ||
|
|
3a09d10d3b | ||
|
|
87de52cae0 | ||
|
|
931fe13eee | ||
|
|
28716eeab2 | ||
|
|
606af3901c | ||
|
|
b2247e79ae | ||
|
|
a6ae770194 | ||
|
|
e378465126 |
@@ -10,3 +10,6 @@ SCORO_URL=ws://localhost:6543
|
||||
GOOGLE_CLIENT_ID=toto
|
||||
GOOGLE_SECRET=tata
|
||||
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
|
||||
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||
IGNORE_MAILS=true
|
||||
|
||||
5
.envrc
5
.envrc
@@ -1,4 +1 @@
|
||||
if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
|
||||
fi
|
||||
use flake
|
||||
use nix
|
||||
|
||||
2
.github/workflows/CI.yml
vendored
2
.github/workflows/CI.yml
vendored
@@ -93,7 +93,7 @@ jobs:
|
||||
run: |
|
||||
docker-compose ps -a
|
||||
docker-compose logs
|
||||
wget --retry-connrefused http://localhost:3000 # /healthcheck
|
||||
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
|
||||
|
||||
- name: Run scorometer tests
|
||||
run: |
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ log.html
|
||||
node_modules/
|
||||
./front/coverage
|
||||
.venv
|
||||
.DS_Store
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/env python3
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
4994
back/package-lock.json
generated
4994
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/common": "^10.1.0",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.1.0",
|
||||
@@ -37,6 +38,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"node-fetch": "^2.6.12",
|
||||
"nodemailer": "^6.9.5",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -53,6 +55,7 @@
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.4.4",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
|
||||
2
back/prisma/migrations/20230907141258_/migration.sql
Normal file
2
back/prisma/migrations/20230907141258_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -14,6 +14,7 @@ model User {
|
||||
username String @unique
|
||||
password String?
|
||||
email String
|
||||
emailVerified Boolean @default(false)
|
||||
googleID String? @unique
|
||||
isGuest Boolean @default(false)
|
||||
partyPlayed Int @default(0)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ArtistModule } from './artist/artist.module';
|
||||
import { AlbumModule } from './album/album.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { HistoryModule } from './history/history.module';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +29,12 @@ import { HistoryModule } from './history/history.module';
|
||||
SearchModule,
|
||||
SettingsModule,
|
||||
HistoryModule,
|
||||
MailerModule.forRoot({
|
||||
transport: process.env.SMTP_TRANSPORT,
|
||||
defaults: {
|
||||
from: process.env.MAIL_AUTHOR,
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, PrismaService, ArtistService],
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
HttpStatus,
|
||||
ParseFilePipeBuilder,
|
||||
Response,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
@@ -71,12 +72,29 @@ export class AuthController {
|
||||
try {
|
||||
const user = await this.usersService.createUser(registerDto);
|
||||
await this.settingsService.createUserSetting(user.id);
|
||||
await this.authService.sendVerifyMail(user);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('verify')
|
||||
async verify(@Request() req: any, @Query('token') token: string): Promise<void> {
|
||||
if (await this.authService.verifyMail(req.user.id, token))
|
||||
return;
|
||||
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('reverify')
|
||||
async reverify(@Request() req: any): Promise<void> {
|
||||
await this.authService.sendVerifyMail(req.user);
|
||||
}
|
||||
|
||||
@ApiBody({ type: LoginDto })
|
||||
@HttpCode(200)
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@@ -121,7 +139,7 @@ export class AuthController {
|
||||
)
|
||||
file: Express.Multer.File,
|
||||
) {
|
||||
const path = `/data/${req.user.id}.jpg`
|
||||
const path = `/data/${req.user.id}.jpg`;
|
||||
writeFile(path, file.buffer, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import PayloadInterface from './interface/payload.interface';
|
||||
import { User } from 'src/models/user';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userService: UsersService,
|
||||
private jwtService: JwtService,
|
||||
private emailService: MailerService,
|
||||
) {}
|
||||
|
||||
async validateUser(
|
||||
@@ -31,4 +34,34 @@ export class AuthService {
|
||||
access_token,
|
||||
};
|
||||
}
|
||||
|
||||
async sendVerifyMail(user: User) {
|
||||
if (process.env.IGNORE_MAILS === "true") return;
|
||||
const token = await this.jwtService.signAsync(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{ expiresIn: '10h' },
|
||||
);
|
||||
await this.emailService.sendMail({
|
||||
to: user.email,
|
||||
from: 'chromacase@octohub.app',
|
||||
subject: 'Mail verification for Chromacase',
|
||||
html: `To verify your mail, please click on this <a href="{${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyMail(userId: number, token: string): Promise<boolean> {
|
||||
try {
|
||||
await this.jwtService.verifyAsync(token);
|
||||
} catch(e) {
|
||||
console.log("Verify mail token failure", e);
|
||||
return false;
|
||||
}
|
||||
await this.userService.updateUser({
|
||||
where: { id: userId },
|
||||
data: { emailVerified: true },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,14 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
export class SongService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async songByArtist(data: number): Promise<Song[]> {
|
||||
return this.prisma.song.findMany({
|
||||
where: {
|
||||
artistId: {equals: data},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createSong(data: Prisma.SongCreateInput): Promise<Song> {
|
||||
return this.prisma.song.create({
|
||||
data,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { User, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
@@ -13,7 +11,9 @@ import fetch from 'node-fetch';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
async user(
|
||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
|
||||
@@ -95,8 +95,7 @@ export class UsersService {
|
||||
const resp = await fetch(
|
||||
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
|
||||
);
|
||||
for (const [k, v] of resp.headers)
|
||||
resp.headers.set(k, v);
|
||||
for (const [k, v] of resp.headers) resp.headers.set(k, v);
|
||||
resp.body!.pipe(res);
|
||||
}
|
||||
}
|
||||
|
||||
43
flake.lock
generated
43
flake.lock
generated
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1665573177,
|
||||
"narHash": "sha256-Arkrf3zmi3lXYpbSe9H+HQxswQ6jxsAmeQVq5Sr/OZc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d2afb051ffd904af5a825f58abee3c63b148c5f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "master",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
31
flake.nix
31
flake.nix
@@ -1,31 +0,0 @@
|
||||
{
|
||||
description = "A prisma test project";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/master";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
nativeBuildInputs = [ pkgs.bashInteractive ];
|
||||
buildInputs = with pkgs; [
|
||||
nodePackages.prisma
|
||||
nodePackages."@nestjs/cli"
|
||||
nodePackages.npm
|
||||
nodejs-slim
|
||||
yarn
|
||||
python3
|
||||
pkg-config
|
||||
];
|
||||
shellHook = with pkgs; ''
|
||||
export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
|
||||
export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
|
||||
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
|
||||
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
|
||||
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
|
||||
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
56
front/API.ts
56
front/API.ts
@@ -21,7 +21,7 @@ import { PlageHandler } from './models/Plage';
|
||||
import { ListHandler } from './models/List';
|
||||
import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
|
||||
import * as yup from 'yup';
|
||||
import { base64ToBlob } from 'file64';
|
||||
import { base64ToBlob } from './utils/base64ToBlob';
|
||||
import { ImagePickerAsset } from 'expo-image-picker';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
@@ -287,6 +287,43 @@ export default class API {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description retrieves songs from a specific artist
|
||||
* @param artistId is the id of the artist that composed the songs aimed
|
||||
* @returns a Promise of Songs type array
|
||||
*/
|
||||
public static getSongsByArtist(artistId: number): Query<Song[]> {
|
||||
return {
|
||||
key: ['artist', artistId, 'songs'],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song?artistId=${artistId}`,
|
||||
},
|
||||
{ handler: PlageHandler(SongHandler) }
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all songs corresponding to the given genre ID
|
||||
* @param genreId the id of the genre we're aiming
|
||||
* @returns a promise of an array of Songs
|
||||
*/
|
||||
public static getSongsByGenre(genreId: number): Query<Song[]> {
|
||||
return {
|
||||
key: ['genre', genreId, 'songs'],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song?genreId=${genreId}`,
|
||||
},
|
||||
{ handler: PlageHandler(SongHandler) }
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive a song's midi partition
|
||||
* @param songId the id to find the song
|
||||
@@ -322,6 +359,23 @@ export default class API {
|
||||
return `${API.baseUrl}/genre/${genreId}/illustration`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a genre
|
||||
* @param genreId the id of the aimed genre
|
||||
*/
|
||||
public static getGenre(genreId: number): Query<Genre> {
|
||||
return {
|
||||
key: ['genre', genreId],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/genre/${genreId}`,
|
||||
},
|
||||
{ handler: GenreHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive a song's musicXML partition
|
||||
* @param songId the id to find the song
|
||||
|
||||
@@ -28,7 +28,9 @@ import { Button, Center, VStack } from 'native-base';
|
||||
import { unsetAccessToken } from './state/UserSlice';
|
||||
import TextButton from './components/TextButton';
|
||||
import ErrorView from './views/ErrorView';
|
||||
import GenreDetailsView from './views/GenreDetailsView';
|
||||
import GoogleView from './views/GoogleView';
|
||||
import VerifiedView from './views/VerifiedView';
|
||||
|
||||
// Util function to hide route props in URL
|
||||
const removeMe = () => '';
|
||||
@@ -59,6 +61,11 @@ const protectedRoutes = () =>
|
||||
options: { title: translate('artistFilter') },
|
||||
link: '/artist/:artistId',
|
||||
},
|
||||
Genre: {
|
||||
component: GenreDetailsView,
|
||||
options: { title: translate('genreFilter') },
|
||||
link: '/genre/:genreId',
|
||||
},
|
||||
Score: {
|
||||
component: ScoreView,
|
||||
options: { title: translate('score'), headerLeft: null },
|
||||
@@ -75,6 +82,11 @@ const protectedRoutes = () =>
|
||||
link: undefined,
|
||||
},
|
||||
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
|
||||
Verified: {
|
||||
component: VerifiedView,
|
||||
options: { title: 'Verify email', headerShown: false },
|
||||
link: '/verify',
|
||||
},
|
||||
} as const);
|
||||
|
||||
const publicRoutes = () =>
|
||||
|
||||
34
front/components/RowCustom.tsx
Normal file
34
front/components/RowCustom.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { RootState, useSelector } from '../state/Store';
|
||||
import { Box, Pressable } from 'native-base';
|
||||
|
||||
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
|
||||
const settings = useSelector((state: RootState) => state.settings.local);
|
||||
const systemColorMode = useColorScheme();
|
||||
const colorScheme = settings.colorScheme;
|
||||
|
||||
return (
|
||||
<Pressable onPress={props.onPress}>
|
||||
{({ isHovered, isPressed }) => (
|
||||
<Box
|
||||
{...props}
|
||||
py={3}
|
||||
my={1}
|
||||
bg={
|
||||
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
|
||||
? isHovered || isPressed
|
||||
? 'gray.800'
|
||||
: undefined
|
||||
: isHovered || isPressed
|
||||
? 'coolGray.200'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowCustom;
|
||||
@@ -1,20 +1,16 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
HStack,
|
||||
VStack,
|
||||
Heading,
|
||||
Text,
|
||||
Pressable,
|
||||
Box,
|
||||
Card,
|
||||
Image,
|
||||
Flex,
|
||||
useBreakpointValue,
|
||||
Column,
|
||||
ScrollView,
|
||||
} from 'native-base';
|
||||
import { SafeAreaView, useColorScheme } from 'react-native';
|
||||
import { RootState, useSelector } from '../state/Store';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { SearchContext } from '../views/SearchView';
|
||||
import { useQueries, useQuery } from '../Queries';
|
||||
import { translate } from '../i18n/i18n';
|
||||
@@ -24,11 +20,11 @@ import ArtistCard from './ArtistCard';
|
||||
import GenreCard from './GenreCard';
|
||||
import SongCard from './SongCard';
|
||||
import CardGridCustom from './CardGridCustom';
|
||||
import TextButton from './TextButton';
|
||||
import SearchHistoryCard from './HistoryCard';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import Artist from '../models/Artist';
|
||||
import SongRow from '../components/SongRow';
|
||||
|
||||
const swaToSongCardProps = (song: SongWithArtist) => ({
|
||||
songId: song.id,
|
||||
@@ -37,101 +33,6 @@ const swaToSongCardProps = (song: SongWithArtist) => ({
|
||||
cover: song.cover ?? 'https://picsum.photos/200',
|
||||
});
|
||||
|
||||
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
|
||||
const settings = useSelector((state: RootState) => state.settings.local);
|
||||
const systemColorMode = useColorScheme();
|
||||
const colorScheme = settings.colorScheme;
|
||||
|
||||
return (
|
||||
<Pressable onPress={props.onPress}>
|
||||
{({ isHovered, isPressed }) => (
|
||||
<Box
|
||||
{...props}
|
||||
py={3}
|
||||
my={1}
|
||||
bg={
|
||||
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
|
||||
? isHovered || isPressed
|
||||
? 'gray.800'
|
||||
: undefined
|
||||
: isHovered || isPressed
|
||||
? 'coolGray.200'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
type SongRowProps = {
|
||||
song: Song | SongWithArtist; // TODO: remove Song
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const SongRow = ({ song, onPress }: SongRowProps) => {
|
||||
return (
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
<Image
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
pl={10}
|
||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
||||
source={{ uri: song.cover }}
|
||||
alt={song.name}
|
||||
/>
|
||||
<HStack
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
space={6}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
isTruncated
|
||||
pl={10}
|
||||
maxW={'100%'}
|
||||
bold
|
||||
fontSize="md"
|
||||
>
|
||||
{song.name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{song.artistId ?? 'artist'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<TextButton
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
translate={{ translationKey: 'playBtn' }}
|
||||
colorScheme="primary"
|
||||
variant={'outline'}
|
||||
size="sm"
|
||||
onPress={onPress}
|
||||
/>
|
||||
</HStack>
|
||||
</RowCustom>
|
||||
);
|
||||
};
|
||||
|
||||
SongRow.defaultProps = {
|
||||
onPress: () => {},
|
||||
};
|
||||
|
||||
const HomeSearchComponent = () => {
|
||||
const { updateStringQuery } = React.useContext(SearchContext);
|
||||
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
|
||||
@@ -252,15 +153,17 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
</Text>
|
||||
{artistData?.length ? (
|
||||
<CardGridCustom
|
||||
content={artistData.slice(0, props.maxItems ?? artistData.length).map((a) => ({
|
||||
image: API.getArtistIllustration(a.id),
|
||||
name: a.name,
|
||||
id: a.id,
|
||||
onPress: () => {
|
||||
API.createSearchHistoryEntry(a.name, 'artist');
|
||||
navigation.navigate('Artist', { artistId: a.id });
|
||||
},
|
||||
}))}
|
||||
content={artistData
|
||||
.slice(0, props.maxItems ?? artistData.length)
|
||||
.map((artistData) => ({
|
||||
image: API.getArtistIllustration(artistData.id),
|
||||
name: artistData.name,
|
||||
id: artistData.id,
|
||||
onPress: () => {
|
||||
API.createSearchHistoryEntry(artistData.name, 'artist');
|
||||
navigation.navigate('Artist', { artistId: artistData.id });
|
||||
},
|
||||
}))}
|
||||
cardComponent={ArtistCard}
|
||||
/>
|
||||
) : (
|
||||
@@ -287,7 +190,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
id: g.id,
|
||||
onPress: () => {
|
||||
API.createSearchHistoryEntry(g.name, 'genre');
|
||||
navigation.navigate('Home');
|
||||
navigation.navigate('Genre', { genreId: g.id });
|
||||
},
|
||||
}))}
|
||||
cardComponent={GenreCard}
|
||||
|
||||
71
front/components/SongRow.tsx
Normal file
71
front/components/SongRow.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { HStack, Image, Text } from 'native-base';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import RowCustom from './RowCustom';
|
||||
import TextButton from './TextButton';
|
||||
|
||||
type SongRowProps = {
|
||||
song: Song | SongWithArtist; // TODO: remove Song
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const SongRow = ({ song, onPress }: SongRowProps) => {
|
||||
return (
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
<Image
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
pl={10}
|
||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
||||
source={{ uri: song.cover }}
|
||||
alt={song.name}
|
||||
borderColor={'white'}
|
||||
borderWidth={1}
|
||||
/>
|
||||
<HStack
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
space={6}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
isTruncated
|
||||
pl={5}
|
||||
maxW={'100%'}
|
||||
bold
|
||||
fontSize="md"
|
||||
>
|
||||
{song.name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{song.artistId ?? 'artist'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<TextButton
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
translate={{ translationKey: 'playBtn' }}
|
||||
colorScheme="primary"
|
||||
variant={'outline'}
|
||||
size="sm"
|
||||
mr={5}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</HStack>
|
||||
</RowCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default SongRow;
|
||||
@@ -18,10 +18,9 @@ const UserAvatar = ({ size }: UserAvatarProps) => {
|
||||
if (!user.data) {
|
||||
return null;
|
||||
}
|
||||
const url = new URL(user.data.data.avatar);
|
||||
|
||||
url.searchParams.append('updatedAt', user.dataUpdatedAt.toString());
|
||||
return url;
|
||||
// NOTE: We do this to avoid parsing URL with `new URL`, which is not compatible with related path
|
||||
// (which is used for production, on web)
|
||||
return `${user.data.data.avatar}?updatedAt=${user.dataUpdatedAt.toString()}`;
|
||||
}, [user.data]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,7 @@ export const en = {
|
||||
artistFilter: 'Artists',
|
||||
songsFilter: 'Songs',
|
||||
genreFilter: 'Genres',
|
||||
favoriteFilter: 'Favorites',
|
||||
|
||||
// profile page
|
||||
user: 'Profile',
|
||||
@@ -182,6 +183,7 @@ export const en = {
|
||||
noRecentSearches: 'No recent searches',
|
||||
avatar: 'Avatar',
|
||||
changeIt: 'Change It',
|
||||
verified: 'Verified',
|
||||
};
|
||||
|
||||
export const fr: typeof en = {
|
||||
@@ -231,6 +233,7 @@ export const fr: typeof en = {
|
||||
artistFilter: 'Artistes',
|
||||
songsFilter: 'Morceaux',
|
||||
genreFilter: 'Genres',
|
||||
favoriteFilter: 'Favoris',
|
||||
|
||||
// Difficulty settings
|
||||
diffBtn: 'Difficulté',
|
||||
@@ -366,6 +369,7 @@ export const fr: typeof en = {
|
||||
noRecentSearches: 'Aucune recherche récente',
|
||||
avatar: 'Avatar',
|
||||
changeIt: 'Modifier',
|
||||
verified: 'Verifié',
|
||||
};
|
||||
|
||||
export const sp: typeof en = {
|
||||
@@ -428,6 +432,7 @@ export const sp: typeof en = {
|
||||
artistFilter: 'Artistas',
|
||||
songsFilter: 'canciones',
|
||||
genreFilter: 'géneros',
|
||||
favoriteFilter: 'Favorites',
|
||||
|
||||
// Difficulty settings
|
||||
diffBtn: 'Dificultad',
|
||||
@@ -555,4 +560,5 @@ export const sp: typeof en = {
|
||||
|
||||
avatar: 'Avatar',
|
||||
changeIt: 'Cambialo',
|
||||
verified: 'Verified',
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export const UserValidator = yup
|
||||
username: yup.string().required(),
|
||||
password: yup.string().required().nullable(),
|
||||
email: yup.string().required(),
|
||||
emailVerified: yup.boolean().required(),
|
||||
googleID: yup.string().required().nullable(),
|
||||
isGuest: yup.boolean().required(),
|
||||
partyPlayed: yup.number().required(),
|
||||
@@ -32,6 +33,7 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
|
||||
interface User extends Model {
|
||||
name: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
googleID: string | null;
|
||||
isGuest: boolean;
|
||||
premium: boolean;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject",
|
||||
"pretty:check": "prettier --check",
|
||||
"pretty:write": "prettier --write",
|
||||
"pretty:check": "prettier --check .",
|
||||
"pretty:write": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"test": "jest -i",
|
||||
"test:cov": "jest -i --coverage",
|
||||
@@ -40,7 +40,6 @@
|
||||
"expo-secure-store": "~12.0.0",
|
||||
"expo-splash-screen": "~0.17.5",
|
||||
"expo-status-bar": "~1.4.2",
|
||||
"file64": "^1.0.2",
|
||||
"format-duration": "^2.0.0",
|
||||
"i18next": "^21.8.16",
|
||||
"install": "^0.13.0",
|
||||
|
||||
23
front/utils/base64ToBlob.ts
Normal file
23
front/utils/base64ToBlob.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// SRC: https://github.com/encrypit/file64/blob/master/src/base64-to-blob.ts
|
||||
export async function base64ToBlob(base64: string): Promise<Blob> {
|
||||
const response = await fetch(base64);
|
||||
let blob = await response.blob();
|
||||
const mimeType = getMimeType(base64);
|
||||
if (mimeType) {
|
||||
// https://stackoverflow.com/a/50875615
|
||||
blob = blob.slice(0, blob.size, mimeType);
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
const mimeRegex = /^data:(.+);base64,/;
|
||||
|
||||
/**
|
||||
* Gets MIME type from Base64.
|
||||
*
|
||||
* @param base64 - Base64.
|
||||
* @returns - MIME type.
|
||||
*/
|
||||
function getMimeType(base64: string) {
|
||||
return base64.match(mimeRegex)?.slice(1, 2).pop();
|
||||
}
|
||||
@@ -1,49 +1,58 @@
|
||||
import { VStack, Image, Heading, IconButton, Icon, Container } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { Box, Heading, useBreakpointValue, ScrollView } from 'native-base';
|
||||
import { useQuery } from '../Queries';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import API from '../API';
|
||||
import { useNavigation } from '../Navigation';
|
||||
|
||||
const handleFavorite = () => {};
|
||||
import Song from '../models/Song';
|
||||
import SongRow from '../components/SongRow';
|
||||
import { Key } from 'react';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import { ImageBackground } from 'react-native';
|
||||
|
||||
type ArtistDetailsViewProps = {
|
||||
artistId: number;
|
||||
};
|
||||
|
||||
const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => {
|
||||
const ArtistDetailsView = ({ artistId }: RouteProps<ArtistDetailsViewProps>) => {
|
||||
const artistQuery = useQuery(API.getArtist(artistId));
|
||||
const songsQuery = useQuery(API.getSongsByArtist(artistId));
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isMobileView = screenSize == 'small';
|
||||
const navigation = useNavigation();
|
||||
const { isLoading, data: artistData, isError } = useQuery(API.getArtist(artistId));
|
||||
|
||||
if (isLoading) {
|
||||
if (artistQuery.isError || songsQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
return <></>;
|
||||
}
|
||||
if (!artistQuery.data || songsQuery.data === undefined) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
navigation.navigate('Error');
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Container m={3}>
|
||||
<Image
|
||||
source={{ uri: 'https://picsum.photos/200' }}
|
||||
alt={artistData?.name}
|
||||
size={20}
|
||||
borderRadius="full"
|
||||
/>
|
||||
<VStack space={3}>
|
||||
<Heading>{artistData?.name}</Heading>
|
||||
<IconButton
|
||||
icon={<Icon as={Ionicons} name="heart" size={6} color="red.500" />}
|
||||
onPress={() => handleFavorite()}
|
||||
variant="unstyled"
|
||||
_pressed={{ opacity: 0.6 }}
|
||||
/>
|
||||
</VStack>
|
||||
</Container>
|
||||
</SafeAreaView>
|
||||
<ScrollView>
|
||||
<ImageBackground
|
||||
style={{ width: '100%', height: isMobileView ? 200 : 300 }}
|
||||
source={{ uri: API.getArtistIllustration(artistQuery.data.id) }}
|
||||
></ImageBackground>
|
||||
<Box>
|
||||
<Heading mt={-20} ml={3} fontSize={50}>
|
||||
{artistQuery.data.name}
|
||||
</Heading>
|
||||
<ScrollView mt={3}>
|
||||
<Box>
|
||||
{songsQuery.data.map((comp: Song, index: Key | null | undefined) => (
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, 'song');
|
||||
navigation.navigate('Song', { songId: comp.id });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</ScrollView>
|
||||
</Box>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
74
front/views/GenreDetailsView.tsx
Normal file
74
front/views/GenreDetailsView.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Flex, Heading, useBreakpointValue, ScrollView } from 'native-base';
|
||||
import { useQueries, useQuery } from '../Queries';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import API from '../API';
|
||||
import CardGridCustom from '../components/CardGridCustom';
|
||||
import SongCard from '../components/SongCard';
|
||||
import { ImageBackground } from 'react-native';
|
||||
|
||||
type GenreDetailsViewProps = {
|
||||
genreId: number;
|
||||
};
|
||||
|
||||
const GenreDetailsView = ({ genreId }: RouteProps<GenreDetailsViewProps>) => {
|
||||
const genreQuery = useQuery(API.getGenre(genreId));
|
||||
const songsQuery = useQuery(API.getSongsByGenre(genreId));
|
||||
const artistQueries = useQueries(
|
||||
songsQuery.data?.map((song) => song.artistId).map((artistId) => API.getArtist(artistId)) ??
|
||||
[]
|
||||
);
|
||||
// Here, .artist will always be defined
|
||||
const songWithArtist = songsQuery?.data
|
||||
?.map((song) => ({
|
||||
...song,
|
||||
artist: artistQueries.find((query) => query.data?.id == song.artistId)?.data,
|
||||
}))
|
||||
.filter((song) => song.artist !== undefined);
|
||||
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isMobileView = screenSize == 'small';
|
||||
const navigation = useNavigation();
|
||||
|
||||
if (genreQuery.isError || songsQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
return <></>;
|
||||
}
|
||||
if (!genreQuery.data || songsQuery.data === undefined || songWithArtist === undefined) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<ImageBackground
|
||||
style={{ width: '100%', height: isMobileView ? 200 : 300 }}
|
||||
source={{ uri: API.getGenreIllustration(genreQuery.data.id) }}
|
||||
></ImageBackground>
|
||||
<Heading ml={3} fontSize={50}>
|
||||
{genreQuery.data.name}
|
||||
</Heading>
|
||||
<Flex
|
||||
flexWrap="wrap"
|
||||
direction={isMobileView ? 'column' : 'row'}
|
||||
justifyContent={['flex-start']}
|
||||
mt={4}
|
||||
>
|
||||
<CardGridCustom
|
||||
content={songWithArtist.map((songData) => ({
|
||||
name: songData.name,
|
||||
cover: songData.cover,
|
||||
artistName: songData.artist!.name,
|
||||
songId: songData.id,
|
||||
onPress: () => {
|
||||
API.createSearchHistoryEntry(songData.name, 'song');
|
||||
navigation.navigate('Song', { songId: songData.id });
|
||||
},
|
||||
}))}
|
||||
cardComponent={SongCard}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenreDetailsView;
|
||||
@@ -77,7 +77,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
const webSocket = useRef<WebSocket>();
|
||||
const [paused, setPause] = useState<boolean>(true);
|
||||
const stopwatch = useStopwatch();
|
||||
const [time, setTime] = useState(0);
|
||||
const time = useRef(0);
|
||||
const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render
|
||||
const [score, setScore] = useState(0); // Between 0 and 100
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
@@ -236,7 +236,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
useEffect(() => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
||||
const interval = setInterval(() => {
|
||||
setTime(() => getElapsedTime()); // Countdown
|
||||
time.current = getElapsedTime(); // Countdown
|
||||
}, 1);
|
||||
|
||||
return () => {
|
||||
@@ -289,7 +289,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
<View style={{ flexGrow: 1, justifyContent: 'center' }}>
|
||||
<PartitionCoord
|
||||
file={musixml.data}
|
||||
timestamp={time}
|
||||
timestamp={time.current}
|
||||
onEndReached={onEnd}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
@@ -342,14 +342,14 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
}}
|
||||
/>
|
||||
<Text>
|
||||
{time < 0
|
||||
{time.current < 0
|
||||
? paused
|
||||
? '0:00'
|
||||
: Math.floor((time % 60000) / 1000)
|
||||
: Math.floor((time.current % 60000) / 1000)
|
||||
.toFixed(0)
|
||||
.toString()
|
||||
: `${Math.floor(time / 60000)}:${Math.floor(
|
||||
(time % 60000) / 1000
|
||||
: `${Math.floor(time.current / 60000)}:${Math.floor(
|
||||
(time.current % 60000) / 1000
|
||||
)
|
||||
.toFixed(0)
|
||||
.toString()
|
||||
|
||||
35
front/views/VerifiedView.tsx
Normal file
35
front/views/VerifiedView.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import API from '../API';
|
||||
import { Text } from 'native-base';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
|
||||
const VerifiedView = () => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
try {
|
||||
await API.fetch({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
route: `/auth/verify?token=${(route.params as any).token}`,
|
||||
method: 'PUT',
|
||||
});
|
||||
navigation.navigate('Home');
|
||||
} catch {
|
||||
setFailed(true);
|
||||
}
|
||||
}
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return failed ? (
|
||||
<Text>Email verification failed. The token has expired or is invalid.</Text>
|
||||
) : (
|
||||
<Text>Loading please wait</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifiedView;
|
||||
@@ -55,6 +55,16 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
title: translate('verified'),
|
||||
data: {
|
||||
text: user.emailVerified ? 'verified' : 'not verified',
|
||||
onPress: user.emailVerified
|
||||
? undefined
|
||||
: () => API.fetch({ route: '/auth/reverify', method: 'PUT' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
title: translate('avatar'),
|
||||
|
||||
@@ -9118,6 +9118,11 @@ expo-keep-awake@~11.0.1:
|
||||
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
|
||||
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==
|
||||
|
||||
expo-linear-gradient@^12.3.0:
|
||||
version "12.3.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.3.0.tgz#7abd8fedbf0138c86805aebbdfbbf5e5fa865f19"
|
||||
integrity sha512-f9e+Oxe5z7fNQarTBZXilMyswlkbYWQHONVfq8MqmiEnW3h9XsxxmVJLG8uVQSQPUsbW+x1UUT/tnU6mkMWeLg==
|
||||
|
||||
expo-linking@~3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"
|
||||
@@ -9451,11 +9456,6 @@ file-uri-to-path@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
file64@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/file64/-/file64-1.0.2.tgz#d3dde9bab142ccf0049e0bd407a2576e94894825"
|
||||
integrity sha512-cDQefGBdb8OO7Pb2nXiRcZlVjwgzoG0uuJ/H2fxNdz3vbOZctp0iPJoHDQ4VZrirqGYc9n/p9+ZqptLZrcSGRA==
|
||||
|
||||
filesize@6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:latest
|
||||
FROM python:3.10
|
||||
RUN wget -q -O /tmp/websocketd.zip \
|
||||
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
|
||||
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:latest
|
||||
FROM python:3.10
|
||||
RUN wget -q -O /tmp/websocketd.zip \
|
||||
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
|
||||
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \
|
||||
|
||||
21
shell.nix
Normal file
21
shell.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{pkgs ? import <nixpkgs> {}}:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = [pkgs.bashInteractive];
|
||||
buildInputs = with pkgs; [
|
||||
nodePackages.prisma
|
||||
nodePackages."@nestjs/cli"
|
||||
nodePackages.npm
|
||||
nodejs_16
|
||||
yarn
|
||||
python3
|
||||
pkg-config
|
||||
];
|
||||
shellHook = with pkgs; ''
|
||||
# export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
|
||||
# export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
|
||||
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
|
||||
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
|
||||
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
|
||||
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user