<repo_name>Thomasan1999/memsource-assignment<file_sep>/.eslintrc.js
module.exports = {
    root: true,
    env: {
        node: true
    },
    overrides: [
        {
            files: '*.vue',
            rules: {
                'max-len': 'off'
            }
        }
    ],
    rules: {
        'arrow-body-style': ['error', 'as-needed', {requireReturnForObjectLiteral: true}],
        'arrow-parens': ['error', 'always'],
        'arrow-spacing': ['error', {after: true, before: true}],
        'brace-style': ['error', 'allman'],
        camelcase: 'off',
        'class-methods-use-this': 'off',
        'comma-dangle': ['error', 'never'],
        'consistent-return': 'off',
        'global-require': 'off',
        indent: ['error', 4, {SwitchCase: 1}],
        'indent-legacy': 'off',
        'import/extensions': 'off',
        'import/no-dynamic-require': 'off',
        'import/prefer-default-export': 'off',
        'newline-per-chained-call': 'off',
        'import/no-cycle': 'off',
        'import/no-extraneous-dependencies': 'off',
        'linebreak-style': ['error', 'windows'],
        'lines-between-class-members': 'off',
        'max-len': ['error', 120],
        'new-cap': 'off',
        'no-console': 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
        'no-floating-decimal': 'off',
        'no-multi-assign': ['error', {ignoreNonDeclaration: true}],
        'no-multi-spaces': 'off',
        'no-param-reassign': ['error', {props: false}],
        'no-undef': 'off',
        'no-unused-vars': 'off',
        'no-use-before-define': 'off',
        'no-restricted-globals': 'off',
        'no-template-curly-in-string': 'off',
        'no-trailing-spaces': 'off',
        'no-underscore-dangle': 'off',
        'object-curly-newline': 'off',
        'object-curly-spacing': ['error', 'never'],
        'object-shorthand': ['error', 'always'],
        'prefer-destructuring': 'off',
        'prefer-promise-reject-errors': 'off',
        'quote-props': 'off',
        quotes: ['error', 'single', {allowTemplateLiterals: true}],
        semi: ['error', 'always'],
        'space-before-function-paren': ['error', {
            anonymous: 'always',
            asyncArrow: 'always',
            named: 'never'
        }],
        'vue/experimental-script-setup-vars': 'off',
        'vue/script-indent': ['off', 4, {
            baseIndent: 1
        }]
    },
    parser: 'vue-eslint-parser',
    parserOptions: {
        parser: '@typescript-eslint/parser'
    },
    settings: {
        'import/resolver': {
            typescript: {}
        }
    }
};
<file_sep>/src/models/ProjectModel.ts
import Model from '@/models/Model';

export default class ProjectModel extends Model
{
    public readonly name!: string;
    public readonly sourceLanguage!: string;
    public readonly status!: 'NEW' | 'COMPLETED' | 'DELIVERED';
    public readonly targetLanguages!: string[];

    constructor(attrs)
    {
        super();
        this.set(attrs);
    }
}
<file_sep>/src/store/index.ts
import {defineStore} from 'pinia';
import {ProjectGetData, ProjectModelFormData, ProjectPostData} from '@/types/project';
import axios from 'axios';
import ProjectModel from '@/models/ProjectModel';
import {cloneDeep} from 'lodash';
import {ModelRaw} from '@/models/Model';
import {ApiResponseData} from '@/types';

const useStore = defineStore('main', {
    state()
    {
        return {
            idCounter: {} as Record<string, number>,
            initialized: false,
            projects: [] as ProjectModel[],
            projectsUrl: '/projects'
        };
    },
    actions: {
        async addProject(formData: ProjectModelFormData): Promise<ProjectModel>
        {
            const dataToSend = {
                ...formData,
                status: 'NEW' as ProjectModel['status']
            };

            const res = await axios.post<ProjectPostData>(this.projectsUrl, dataToSend);

            const resDataParsed = this.makeResponseDataStorable(res.data);

            const projectModel = new ProjectModel(resDataParsed);

            this.projects.push(projectModel);

            return projectModel;
        },
        async deleteProject(projectToDelete: ProjectModel): Promise<void>
        {
            await axios.delete(this.getProjectUrl(projectToDelete));

            this.projects = this.projects.filter((project) => project.id !== projectToDelete.id);
        },
        getProjectUrl(project: ProjectModel): string
        {
            return `${this.projectsUrl}/${project.id}`;
        },
        async fetchProjects(): Promise<void>
        {
            const res = await axios.get<ProjectGetData>(this.projectsUrl);

            this.projects = res.data._embedded.projects.map((project) => new ProjectModel(project));
        },
        incrementIdCounter(counterName: string): void
        {
            this.idCounter[counterName] ??= 0;
            this.idCounter[counterName]++;
        },
        async init(): Promise<void>
        {
            if (this.initialized)
            {
                throw new Error('State has been already initialized');
            }

            await this.fetchProjects();
        },
        makeResponseDataStorable(resData: ApiResponseData): ModelRaw
        {
            const resDataCopy: any = cloneDeep(resData);

            delete resDataCopy._links;

            return resDataCopy;
        },
        async saveProject(projectToSave: ProjectModel, formData: ProjectModelFormData): Promise<ProjectModel>
        {
            const res = await axios.patch<ProjectPostData>(this.getProjectUrl(projectToSave), formData);

            const projectModel = this.projects.find((project) => project.id === res.data.id)! as ProjectModel;

            const resDataParsed = this.makeResponseDataStorable(res.data);

            projectModel.set(resDataParsed);

            return projectModel;
        }
    }
});

export default useStore;
<file_sep>/src/models/Model.ts
import {DataType, ExtractPropertyNames, PartialRecord} from '@/types';
import {Merge} from 'ts-essentials';
import dayjs, {Dayjs} from 'dayjs';

export type ModelRaw<T extends Model = Model> = Merge<
    T,
    {[s in ExtractPropertyNames<T, Dayjs | null>]: string | null}
>;

export default class Model
{
    public readonly id!: number;
    public readonly dateCreated!: Dayjs | null;
    public readonly dateDue!: Dayjs | null;
    public readonly dateUpdated!: Dayjs | null;

    public set(attrs: Record<keyof Model, any>): void
    {
        Object.entries(attrs).forEach(([key, value]) =>
        {
            this[key] = this.transforms[key] ? this.transformValue(key, value) : value;
        });
    }

    protected get transforms(): PartialRecord<keyof Model, DataType>
    {
        return {
            dateCreated: 'date',
            dateDue: 'date',
            dateUpdated: 'date'
        };
    }

    private transformValue(key: string, value: any): any
    {
        if (value === null || value === undefined)
        {
            return value;
        }

        switch (this.transforms[key] as DataType)
        {
            case 'date':
                return dayjs(value);
        }
    }
}
<file_sep>/vite.config.ts
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
import {defineConfig} from 'vite';
import eslintPlugin from 'vite-plugin-eslint';
import {minifyHtml} from 'vite-plugin-html';

export default defineConfig({
    plugins: [
        eslintPlugin({
            cache: false
        }),
        vue(),
        minifyHtml()
    ],
    resolve: {
        alias: {
            '@': path.resolve(__dirname, './src')
        }
    },
    server: {
        port: 8080
    }
});
<file_sep>/src/types/index.d.ts
import {Merge} from 'ts-essentials';
import {ModelRaw} from '@/models/Model';

export type ApiResponseData<T extends ModelRaw = ModelRaw> = Merge<T, {_links: ApiResponseLinks}>;

export type ApiResponseLinks = Record<string, {href: string}>;

export type DataType = 'boolean' | 'date' | 'number' | 'string' | 'string[]';

type ExtractPropertyNames<Object, Type> = {
    [K in keyof Object]: Object[K] extends Type ? K : never
}[keyof Object]

export type PartialRecord<K extends keyof any, T> = {
    [P in K]?: T;
};

export type SortOrder = 1 | -1;

export type TableColumn = {name: string, sortable?: boolean, type: DataType};
<file_sep>/src/types/project.d.ts
import ProjectModel from '@/models/ProjectModel';
import {ModelRaw} from '@/models/Model';
import {ApiResponseData} from '@/types/index';

export type ProjectModelFormData = Pick<ProjectModel, 'name' | 'sourceLanguage' | 'targetLanguages'>;

export type ProjectGetData = ApiResponseData<{
    _embedded: {projects: ModelRaw<ProjectModel>[]}
}>;

export type ProjectPostData = ApiResponseData<ModelRaw<ProjectModel>>;
<file_sep>/src/locales/en.ts
export default {
    add: 'Add',
    form: {
        deleteInput: 'Delete Input',
        submit: {
            add: 'Submit',
            edit: 'Save'
        }
    },
    goBack: 'Go back',
    project: {
        id: 'Id',
        actions: {
            add: 'Create new project',
            delete: 'Delete project',
            edit: 'Edit project',
            reset: 'Reset form'
        },
        dateDue: 'Date Due',
        name: 'Name',
        saveSuccessful: 'Project has been successfully saved.',
        sourceLanguage: 'Source Language',
        status: 'Status',
        targetLanguages: 'Target Languages'
    },
    projects: 'Projects',
    totalOverview: {
        dueDateInPast: 'No. of projects with due date in the past',
        heading: 'Total Overview',
        mostProminent: 'Most prominent source languages',
        projectsByStatus: 'No. of projects by the status'
    }
};
<file_sep>/src/main.ts
import {createApp} from 'vue';
import {createPinia} from 'pinia';
import App from '@/App.vue';
import {createI18n} from 'vue-i18n';
import en from '@/locales/en';
import router from '@/router';
import VueAxios from 'vue-axios';
import axios from 'axios';
import dayjs from 'dayjs';

const app = createApp(App);

const i18n = createI18n({
    locale: 'en',
    messages: {
        en
    }
});

app.use(createPinia()).use(i18n);
app.use(router);
app.use(VueAxios, axios);

app.config.globalProperties.$dayjs = dayjs;

axios.defaults.baseURL = import.meta.env.VITE_API_URL;

app.mount('#app');
<file_sep>/src/router/index.ts
import {createRouter, createWebHistory} from 'vue-router';
import ProjectCreate from '@/components/projects/ProjectCreate.vue';
import ProjectEdit from '@/components/projects/ProjectEdit.vue';
import Projects from '@/components/projects/Projects.vue';
import App from '@/App.vue';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/projects',
            component: App,
            children: [
                {
                    path: 'create',
                    name: 'ProjectCreate',
                    component: ProjectCreate
                },
                {
                    path: ':id',
                    name: 'ProjectEdit',
                    component: ProjectEdit
                },
                {
                    path: '',
                    name: 'Projects',
                    component: Projects
                }
            ]
        },
        {
            path: '/',
            redirect: '/projects'
        }
    ]
});

export default router;
