Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Frontend POC #243

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ reup: down up

build:
docker-compose build --no-cache
make yarn_install
make up
make db_mysql_create_database
make composer_install
make db_mysql_create_database
make app_database_migrate
make exec_app_cmd CMD="php bin/console.php storage:link"

Expand All @@ -34,6 +35,12 @@ exec_mysql_cli:
exec_mysql_query:
docker-compose exec mysql bash -c "mysql -uroot -p${DATABASE_MYSQL_ROOT_PASSWORD} -e \"$(QUERY)\""

exec_frontend_cmd:
docker-compose exec frontend bash -c "${CMD}"

run_frontend_cmd:
docker-compose run --rm frontend bash -c "${CMD}"

# Composer
##########
composer_install:
Expand Down Expand Up @@ -95,3 +102,11 @@ app_imdb_sync_rating:

app_jobs_process:
make exec_app_cmd CMD="php bin/console.php jobs:process"

# Yarn
######
yarn_install:
make run_frontend_cmd CMD="yarn install"

yarn_build:
make exec_frontend_cmd CMD="yarn build"
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,15 @@ services:
ports:
- "${DATABASE_MYSQL_PORT}:3306"

frontend:
image: node:18.3.0
user: "${USER_ID}:${USER_ID}"
working_dir: /app/frontend
command: sh -c "yarn && yarn dev"
ports:
- "5173:5173"
volumes:
- ./:/app

volumes:
movary-database:
33 changes: 33 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
823 changes: 823 additions & 0 deletions frontend/.yarn/releases/yarn-3.3.1.cjs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions frontend/.yarnrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


yarn-path ".yarn/releases/yarn-1.22.19.cjs"
3 changes: 3 additions & 0 deletions frontend/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-3.3.1.cjs
33 changes: 33 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "movary-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@mantine/core": "^5.10.1",
"@mantine/form": "^5.10.1",
"@mantine/hooks": "^5.10.1",
"i18next": "^22.4.9",
"i18next-browser-languagedetector": "^7.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.1.4",
"react-query": "^3.39.2",
"react-router-dom": "^6.7.0"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react-swc": "^3.0.0",
"typescript": "^4.9.3",
"vite": "^4.0.0"
},
"packageManager": "yarn@3.3.1"
}
36 changes: 36 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MantineProvider } from '@mantine/core'
import Login from './pages/login/Login'
import { Suspense } from 'react'
import Loader from './components/Loader'
import { QueryClient, QueryClientProvider } from 'react-query'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import './i18next.config'

const App = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
})

const router = createBrowserRouter([
{
path: "/",
element: <Login />,
},
]);

return (
<MantineProvider withNormalizeCSS withGlobalStyles>
<Suspense fallback={<Loader/>}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</Suspense>
</MantineProvider>
)
}

export default App
9 changes: 9 additions & 0 deletions frontend/src/components/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Center, Loader as MantineLoader } from "@mantine/core";

const Loader = () => (
<Center style={{height: '100%', width: '100%'}}>
<MantineLoader/>
</Center>
)

export default Loader;
19 changes: 19 additions & 0 deletions frontend/src/i18next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import i18n, { Resource } from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import { ViteBackend, ViteBackendOptions } from "./vite.backend";

i18n
.use(LanguageDetector)
.use(ViteBackend)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'es'],
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});

export default i18n;
9 changes: 9 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
57 changes: 57 additions & 0 deletions frontend/src/pages/login/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button, Checkbox, Stack, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQueryClient } from "react-query";
import { login } from "../../repositories/auth";

const Login = () => {
const {i18n, t} = useTranslation('login');

const form = useForm({
initialValues: {
email: '',
password: '',
rememberMe: false,
},

validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
},
});

const queryClient = useQueryClient()
const mutation = useMutation(login, {
onSuccess: () => {
queryClient.invalidateQueries('user')
},
})

return (
<Stack style={{ height: "100%" }} justify="center" align="center">
<Title order={1}>Movary</Title>
<form onSubmit={form.onSubmit((values) => mutation.mutate(values))} style={{ display: 'inline-block' }}>
<TextInput
size="lg"
label={t('email')}
{...form.getInputProps('email')}
/>
<TextInput
mt="lg"
size="lg"
label={t('password')}
type="password"
{...form.getInputProps('password')}
/>
<Checkbox
mt="lg"
label={t('remember')}
{...form.getInputProps('rememberme', { type: 'checkbox' })}
/>
<Button size="lg" mt="lg" loading={mutation.isLoading} fullWidth type="submit">Sign in</Button>
</form>
</Stack>
)
}

export default Login
5 changes: 5 additions & 0 deletions frontend/src/pages/login/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"email": "Email",
"password": "Password",
"remember": "Remember me"
}
28 changes: 28 additions & 0 deletions frontend/src/repositories/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
type Login = {
email: string,
password: string,
rememberMe: boolean
}

type LoginBody = {
email: string,
password: string,
rememberMe?: string
}

export const login = async ({rememberMe, ...login}: Login) => {
const body: LoginBody = {
...login
}
if (rememberMe) {
body.rememberMe = '1'
}

await fetch('/login', {
method: 'POST',
headers:{
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(body)
});
}
1 change: 1 addition & 0 deletions frontend/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
44 changes: 44 additions & 0 deletions frontend/src/vite.backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { BackendModule, InitOptions, ModuleType, ReadCallback, ResourceKey, Services, TOptions } from 'i18next'

export type ViteBackendOptions = {}

export class ViteBackend implements BackendModule {
private services: Services;
private options: TOptions;
private allOptions: InitOptions;
private modules: Record<string, () => Promise<unknown>>;
public type: 'backend' = 'backend';
static type: 'backend' = 'backend';

constructor (services: Services, options: ViteBackendOptions, allOptions = {}) {
this.services = services;
this.options = options;
this.allOptions = allOptions;
this.modules = {};
this.init(services, options, allOptions)
}

init (services: Services, options: ViteBackendOptions, allOptions = {}) {
this.services = services
this.options = options;
this.allOptions = allOptions;
this.modules = import.meta.glob('/src/pages/**/locales/*.json');
}

async read (language: string, namespace: string, callback: ReadCallback) {
for (const key in this.modules) {
const [,,,moduleNamespace,,localeFile] = key.split('/');
const [moduleLanguage] = localeFile.split('.');

if (language !== moduleLanguage || namespace !== moduleNamespace) continue;

this.modules[key]().then((value) => {
callback(null, value as ResourceKey)
}, (error) => {
callback(error, null)
});
break;
}
callback(null, {})
}
}
21 changes: 21 additions & 0 deletions frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
9 changes: 9 additions & 0 deletions frontend/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
Loading