experiments with docker packaging, figuring out deploy plan

This commit is contained in:
Amruth Pillai 2022-03-03 12:24:32 +01:00
parent 2aa3786f5f
commit 58160b2b6e
No known key found for this signature in database
GPG Key ID: E3C57DF9B80855AD
20 changed files with 1068 additions and 372 deletions

View File

@ -1,4 +1,3 @@
.git
dist
Dockerfile
node_modules

View File

@ -4,12 +4,8 @@ WORKDIR /app
COPY ./package*.json .
RUN npm config set unsafe-perm true
RUN npm ci
RUN npm install sharp --ignore-scripts=false
COPY . .
RUN npm run build

View File

@ -1,2 +1,8 @@
NEXT_PUBLIC_APP_VERSION=$npm_package_version
# App & Server URLs
NEXT_PUBLIC_APP_URL=$APP_URL
NEXT_PUBLIC_SERVER_URL=$SERVER_URL
# Google OAuth
NEXT_PUBLIC_GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID

18
apps/client/i18n/index.ts Normal file
View File

@ -0,0 +1,18 @@
import HttpBackend from 'i18next-http-backend';
const i18nConfig = {
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
debug: false,
nsSeparator: '.',
ns: ['common', 'modals', 'landing', 'dashboard', 'builder'],
serializeConfig: false,
use: [HttpBackend],
backend: {
loadPath: `${process.env.NEXT_PUBLIC_APP_URL}/locales/{{lng}}/{{ns}}.json`,
},
};
export default i18nConfig;

View File

@ -1,14 +0,0 @@
/**
* @type {import('next-i18next').UserConfig}
**/
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
debug: false,
nsSeparator: '.',
initImmediate: false,
localePath: './apps/client/public/locales',
ns: ['common', 'modals', 'landing', 'dashboard', 'builder'],
};

View File

@ -1,12 +1,13 @@
const withNx = require('@nrwl/next/plugins/with-nx');
const { i18n } = require('./next-i18next.config');
/**
* @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
**/
const nextConfig = {
i18n,
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
nx: {
svgr: false,

View File

@ -4,11 +4,11 @@ import { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import { useTranslation } from 'next-i18next';
import i18nConfig from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useEffect } from 'react';
import { useQuery } from 'react-query';
import i18nConfig from '@/i18n/index';
import { fetchResumeByIdentifier } from '@/services/resume';
import { useAppDispatch } from '@/store/hooks';
import { setResume } from '@/store/resume/resumeSlice';

View File

@ -9,12 +9,12 @@ import { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { useRouter } from 'next/router';
import i18nConfig from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useEffect } from 'react';
import toast from 'react-hot-toast';
import { useMutation, useQuery } from 'react-query';
import i18nConfig from '@/i18n/index';
import { ServerError } from '@/services/axios';
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
import { fetchResumeByIdentifier } from '@/services/resume';
@ -39,7 +39,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ query, loc
const { username, slug } = query as QueryParams;
try {
const resume = await fetchResumeByIdentifier({ username, slug, options: { withHost: true } });
const resume = await fetchResumeByIdentifier({ username, slug });
return {
props: { username, slug, resume, ...(await serverSideTranslations(locale, ['common'], i18nConfig)) },

View File

@ -31,7 +31,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ query }) =
try {
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey, withHost: true } });
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
return { props: { resume } };
} catch (error) {

View File

@ -4,36 +4,28 @@ import DateAdapter from '@mui/lab/AdapterDayjs';
import LocalizationProvider from '@mui/lab/LocalizationProvider';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { appWithTranslation, useTranslation } from 'next-i18next';
import i18nConfig from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { appWithTranslation } from 'next-i18next';
import { Toaster } from 'react-hot-toast';
import { QueryClientProvider } from 'react-query';
import { Provider as ReduxProvider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import Loading from '@/components/shared/Loading';
import i18nConfig from '@/i18n/index';
import ModalWrapper from '@/modals/index';
import queryClient from '@/services/react-query';
import store, { persistor } from '@/store/index';
import WrapperRegistry from '@/wrappers/index';
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'modals'], i18nConfig)),
},
};
}
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t('common.title')}</title>
<meta name="description" content={t('common.description')} />
<title>Reactive Resume</title>
<meta
name="description"
content="Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3."
/>
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="initial-scale=1, width=device-width" />

View File

@ -4,11 +4,11 @@ import dynamic from 'next/dynamic';
import Head from 'next/head';
import Link from 'next/link';
import { useTranslation } from 'next-i18next';
import i18nConfig from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useQuery } from 'react-query';
import { RESUMES_QUERY } from '@/constants/index';
import i18nConfig from '@/i18n/index';
import { fetchResumes } from '@/services/resume';
import styles from '@/styles/pages/Dashboard.module.scss';

View File

@ -1,14 +1,14 @@
import { Link as LinkIcon } from '@mui/icons-material';
import { Button } from '@mui/material';
import type { NextPage } from 'next';
import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import Image from 'next/image';
import Link from 'next/link';
import { Trans, useTranslation } from 'next-i18next';
import i18nConfig from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { screenshots } from '@/config/screenshots';
import i18nConfig from '@/i18n/index';
import { logout } from '@/store/auth/authSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
@ -20,13 +20,13 @@ const Footer = dynamic(() => import('@/components/shared/Footer'));
const Logo = dynamic(() => import('@/components/shared/Logo'));
const NoSSR = dynamic(() => import('@/components/shared/NoSSR'));
export async function getStaticProps({ locale }) {
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'], i18nConfig)),
},
};
}
};
const Home: NextPage = () => {
const { t } = useTranslation();

View File

@ -8,12 +8,12 @@ import isEmpty from 'lodash/isEmpty';
import { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import i18nConfig from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useEffect } from 'react';
import toast from 'react-hot-toast';
import { useMutation, useQuery } from 'react-query';
import i18nConfig from '@/i18n/index';
import { ServerError } from '@/services/axios';
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
import { fetchResumeByShortId } from '@/services/resume';
@ -36,7 +36,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ query, loc
const { shortId } = query as QueryParams;
try {
const resume = await fetchResumeByShortId({ shortId, options: { withHost: true } });
const resume = await fetchResumeByShortId({ shortId });
return { props: { shortId, resume, ...(await serverSideTranslations(locale, ['common'], i18nConfig)) } };
} catch {

View File

@ -1,6 +0,0 @@
{
"/api": {
"target": "http://localhost:3100",
"secure": false
}
}

View File

@ -13,11 +13,11 @@ export type ServerError = {
};
const axios = _axios.create({
baseURL: '/api',
baseURL: `${process.env.NEXT_PUBLIC_SERVER_URL}/api`,
});
export const uninterceptedAxios = _axios.create({
baseURL: '/api',
baseURL: `${process.env.NEXT_PUBLIC_SERVER_URL}/api`,
});
axios.interceptors.request.use((config) => {

View File

@ -14,7 +14,6 @@ export type FetchResumeByIdentifierParams = {
username: string;
slug: string;
options?: {
withHost?: boolean;
secretKey?: string;
};
};
@ -22,7 +21,6 @@ export type FetchResumeByIdentifierParams = {
export type FetchResumeByShortIdParams = {
shortId: string;
options?: {
withHost?: boolean;
secretKey?: string;
};
};
@ -60,25 +58,20 @@ export type DeleteResumeParams = {
export const fetchResumes = () => axios.get<Resume[]>('/resume').then((res) => res.data);
export const fetchResumeByShortId = async ({
shortId,
options = { secretKey: '', withHost: false },
}: FetchResumeByShortIdParams) => {
const hostname = options.withHost ? `${process.env.SERVER_URL}/api` : '';
export const fetchResumeByShortId = async ({ shortId, options = { secretKey: '' } }: FetchResumeByShortIdParams) => {
const requestOptions = isEmpty(options.secretKey) ? {} : { params: { secretKey: options.secretKey } };
return axios.get<Resume>(`${hostname}/resume/short/${shortId}`, requestOptions).then((res) => res.data);
return axios.get<Resume>(`/resume/short/${shortId}`, requestOptions).then((res) => res.data);
};
export const fetchResumeByIdentifier = async ({
username,
slug,
options = { secretKey: '', withHost: false },
options = { secretKey: '' },
}: FetchResumeByIdentifierParams) => {
const hostname = options.withHost ? `${process.env.SERVER_URL}/api` : '';
const requestOptions = isEmpty(options.secretKey) ? {} : { params: { secretKey: options.secretKey } };
return axios.get<Resume>(`${hostname}/resume/${username}/${slug}`, requestOptions).then((res) => res.data);
return axios.get<Resume>(`/resume/${username}/${slug}`, requestOptions).then((res) => res.data);
};
export const createResume = (createResumeParams: CreateResumeParams) =>

View File

@ -16,12 +16,13 @@
"paths": {
"@/components/*": ["components/*"],
"@/config/*": ["config/*"],
"@/constants/*": ["constants/*"],
"@/i18n/*": ["i18n/*"],
"@/modals/*": ["modals/*"],
"@/pages/*": ["pages/*"],
"@/public/*": ["public/*"],
"@/services/*": ["services/*"],
"@/store/*": ["store/*"],
"@/constants/*": ["constants/*"],
"@/styles/*": ["styles/*"],
"@/templates/*": ["templates/*"],
"@/types/*": ["types/*"],

20
libs/schema/LICENSE.md Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2020-2022 Amruth Pillai
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1291
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,9 @@
"serve": "nx run-many --target=serve --all",
"test": "nx run-many --target=test --all",
"lint": "nx run-many --target=lint --all --fix",
"start": "nx run-many --target=serve --all --prod",
"start:server": "node dist/apps/server/main",
"start:client": "cd dist/apps/client && npm start",
"start": "concurrently --kill-others \"npm run start:server\" \"npm run start:client\"",
"build": "nx run-many --target=build --all --prod",
"format": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\"",
"prepare": "husky install",
@ -55,6 +57,7 @@
"downloadjs": "^1.4.7",
"googleapis": "^95.0.0",
"handlebars": "^4.7.7",
"i18next-http-backend": "^1.3.2",
"joi": "^17.6.0",
"lodash": "^4.17.21",
"md5-hex": "^4.0.0",
@ -93,6 +96,7 @@
"regenerator-runtime": "0.13.9",
"remark-gfm": "^3.0.1",
"rxjs": "^7.5.4",
"sharp": "^0.30.2",
"tailwindcss": "^3.0.23",
"tslib": "^2.3.1",
"typeorm": "^0.2.44",
@ -138,6 +142,7 @@
"@typescript-eslint/parser": "~5.10.2",
"babel-jest": "27.5.1",
"babel-loader": "^8.2.3",
"concurrently": "^7.0.0",
"csstype": "^3.0.10",
"cypress": "^9.5.1",
"eslint": "~8.10.0",