create-jwn-js 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +11 -0
- package/package.json +1 -1
- package/template-vite-vue3-ts/.gitignore +5 -0
- package/template-vite-vue3-ts/connect-template.json +36 -0
- package/template-vite-vue3-ts/cron.js +90 -0
- package/template-vite-vue3-ts/docs/index.md +27 -0
- package/template-vite-vue3-ts/index.html +18 -0
- package/template-vite-vue3-ts/jest.config.ts +38 -0
- package/template-vite-vue3-ts/package.json +57 -0
- package/template-vite-vue3-ts/postcss.config.js +28 -0
- package/template-vite-vue3-ts/src/backend/server/helpers/createConfigServer.ts +16 -0
- package/template-vite-vue3-ts/src/backend/server/helpers/createMariadb.ts +16 -0
- package/template-vite-vue3-ts/src/backend/server/helpers/createMemcached.ts +15 -0
- package/template-vite-vue3-ts/src/backend/server/helpers/readManifest.ts +16 -0
- package/template-vite-vue3-ts/src/backend/server/index.ts +51 -0
- package/template-vite-vue3-ts/src/backend/server/site-schema.ts +21 -0
- package/template-vite-vue3-ts/src/backend/server/web-routes/index.ts +9 -0
- package/template-vite-vue3-ts/src/backend/views/Index/IndexController.ts +16 -0
- package/template-vite-vue3-ts/src/frontend/App.vue +30 -0
- package/template-vite-vue3-ts/src/frontend/entry-client.ts +2 -0
- package/template-vite-vue3-ts/src/frontend/entry-server.ts +4 -0
- package/template-vite-vue3-ts/src/frontend/helpers/customerPrefetch.ts +46 -0
- package/template-vite-vue3-ts/src/frontend/main.ts +68 -0
- package/template-vite-vue3-ts/src/frontend/routes/index.ts +42 -0
- package/template-vite-vue3-ts/src/frontend/shims-plugins.d.ts +18 -0
- package/template-vite-vue3-ts/src/frontend/shims-vue.d.ts +5 -0
- package/template-vite-vue3-ts/src/frontend/shims-vuex.d.ts +10 -0
- package/template-vite-vue3-ts/src/frontend/store/fetch.ts +174 -0
- package/template-vite-vue3-ts/src/frontend/store/index.ts +131 -0
- package/template-vite-vue3-ts/src/frontend/store/types.d.ts +83 -0
- package/template-vite-vue3-ts/src/frontend/views/Error/Error.vue +62 -0
- package/template-vite-vue3-ts/src/frontend/views/Index/Index.vue +27 -0
- package/template-vite-vue3-ts/src/frontend/views/Index/__tests__/Index.spec.ts +31 -0
- package/template-vite-vue3-ts/src/frontend/vite-env.d.ts +1 -0
- package/template-vite-vue3-ts/src/helpers/langLink.ts +17 -0
- package/template-vite-vue3-ts/src/helpers/parseLang.ts +10 -0
- package/template-vite-vue3-ts/src/scripts/jest.global.setup.ts +23 -0
- package/template-vite-vue3-ts/src/scripts/jest.global.teardown.ts +10 -0
- package/template-vite-vue3-ts/src/scripts/jest.setup.ts +16 -0
- package/template-vite-vue3-ts/tsconfig.json +47 -0
- package/template-vite-vue3-ts/vite.config.ts +87 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {createRouter, createMemoryHistory, createWebHistory} from "vue-router";
|
|
2
|
+
import Error from "../views/Error/Error.vue";
|
|
3
|
+
import Index from "../views/Index/Index.vue";
|
|
4
|
+
|
|
5
|
+
export default (opt: Record<string, any>) => createRouter({
|
|
6
|
+
history: opt.isClient ? createWebHistory() : createMemoryHistory(),
|
|
7
|
+
routes: [
|
|
8
|
+
{
|
|
9
|
+
path: '/:lang(ua|ru|en)?',
|
|
10
|
+
component: Index,
|
|
11
|
+
name: "Index"
|
|
12
|
+
},
|
|
13
|
+
// Error
|
|
14
|
+
{
|
|
15
|
+
path: '/:code(400|401|402|403|404)',
|
|
16
|
+
name: 'Error',
|
|
17
|
+
component: Error,
|
|
18
|
+
props: true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: '/:path(.*)',
|
|
22
|
+
redirect: '/404'
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
async scrollBehavior(to, from, savedPosition) {
|
|
26
|
+
|
|
27
|
+
// Scroll to element
|
|
28
|
+
if(to.query?.el) {
|
|
29
|
+
const el: string = to.query.el as string;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
el
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// meta scrollToTop
|
|
37
|
+
if(to.meta.scrollToTop) {
|
|
38
|
+
|
|
39
|
+
return {left: 0, top: 0};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ComponentCustomProperties, ComponentCustomOptions } from "vue";
|
|
2
|
+
import {AmittEmitter} from "amitt";
|
|
3
|
+
import type {NavigationGuardFetchWithThis} from "@vuemod/prefetch";
|
|
4
|
+
|
|
5
|
+
declare module '@vue/runtime-core' {
|
|
6
|
+
interface ComponentCustomProperties {
|
|
7
|
+
$getLang: () => string,
|
|
8
|
+
$l:(params: Record<string, any>) => string,
|
|
9
|
+
$smoothScroll:(params: Record<string, any>) => string,
|
|
10
|
+
$emitter: AmittEmitter,
|
|
11
|
+
$refs: Record<string, any>,
|
|
12
|
+
$confirm: Record<string, any>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ComponentCustomOptions {
|
|
16
|
+
prefetch?: NavigationGuardFetchWithThis<undefined>
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {isBrowser} from "easy-ash";
|
|
2
|
+
import fetch from "cross-fetch";
|
|
3
|
+
import {ApiError} from "@jwn-js/common/ApiError";
|
|
4
|
+
import type {ActionContext} from "vuex";
|
|
5
|
+
|
|
6
|
+
import type {FetchOptions, FetchPayload} from "./types";
|
|
7
|
+
import {StateBase} from "./index";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch action
|
|
11
|
+
* @param state store state
|
|
12
|
+
* @param commit
|
|
13
|
+
* @param getters store getters
|
|
14
|
+
* @param dispatch
|
|
15
|
+
* @param params payload params
|
|
16
|
+
* @param options request options
|
|
17
|
+
*/
|
|
18
|
+
export const fetchAction = async(
|
|
19
|
+
{state, commit, getters, dispatch}: ActionContext<StateBase, StateBase>,
|
|
20
|
+
[
|
|
21
|
+
params = {},
|
|
22
|
+
options
|
|
23
|
+
]: FetchPayload
|
|
24
|
+
): Promise<any> => {
|
|
25
|
+
const headers: Record<string, string> = {};
|
|
26
|
+
try {
|
|
27
|
+
const isFormData = typeof FormData !== "undefined" && params instanceof FormData;
|
|
28
|
+
const ssrHeaders = (state.context.headers || {});
|
|
29
|
+
|
|
30
|
+
// Add auto lang param
|
|
31
|
+
if(isFormData) {
|
|
32
|
+
if(!params.has("lang")) {
|
|
33
|
+
params.append("lang", state.lang);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
if(!(params as Record<string, any>).lang) {
|
|
37
|
+
(params as Record<string, any>).lang = state.lang;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if(isBrowser()) {
|
|
42
|
+
[
|
|
43
|
+
"host",
|
|
44
|
+
"connection",
|
|
45
|
+
"user-agent",
|
|
46
|
+
"referer",
|
|
47
|
+
"accept-encoding",
|
|
48
|
+
"cookie",
|
|
49
|
+
"sec-ch-ua",
|
|
50
|
+
"sec-ch-ua-mobile",
|
|
51
|
+
"sec-fetch-site",
|
|
52
|
+
"sec-fetch-mode",
|
|
53
|
+
"sec-fetch-dest",
|
|
54
|
+
"sec-fetch-user",
|
|
55
|
+
"sec-ch-ua-platform"
|
|
56
|
+
].map(key => delete ssrHeaders[key]);
|
|
57
|
+
} else {
|
|
58
|
+
// if server request - add ip to x-forwarded-for
|
|
59
|
+
ssrHeaders["x-forwarded-for"] = state.context.ip;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const defaultOptions: FetchOptions = {
|
|
63
|
+
url: `${getters.getBaseUrl}/web`,
|
|
64
|
+
method: "get",
|
|
65
|
+
preloader: true,
|
|
66
|
+
error: true,
|
|
67
|
+
compress: true,
|
|
68
|
+
headers: {
|
|
69
|
+
// Proxy ssr headers to request
|
|
70
|
+
...ssrHeaders,
|
|
71
|
+
...(isFormData ? {} : {"content-type": "application/json"})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
options = Object.assign(
|
|
75
|
+
{},
|
|
76
|
+
defaultOptions,
|
|
77
|
+
options || {}
|
|
78
|
+
) as FetchOptions;
|
|
79
|
+
|
|
80
|
+
if(options?.preloader) {
|
|
81
|
+
commit('SET_LOADING', true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge headers
|
|
85
|
+
if(options.mergeHeaders) {
|
|
86
|
+
options.headers = Object.assign(options.headers || {}, options.mergeHeaders);
|
|
87
|
+
delete options.mergeHeaders;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const url = new URL(options.url as string);
|
|
91
|
+
|
|
92
|
+
if(!["get", "head"].includes(options?.method?.toLowerCase() as string)) {
|
|
93
|
+
|
|
94
|
+
if(isFormData) {
|
|
95
|
+
options.body = params as FormData;
|
|
96
|
+
} else {
|
|
97
|
+
options.body = JSON.stringify(params);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
url.search = new URLSearchParams(params as Record<string, any>).toString();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const response = await fetch(url.toString(), options);
|
|
104
|
+
response.headers.forEach((value: string, key: string) => headers[key] = value);
|
|
105
|
+
const json = await response.json();
|
|
106
|
+
|
|
107
|
+
// status code error
|
|
108
|
+
if(!response.ok) {
|
|
109
|
+
throw new ApiError({
|
|
110
|
+
code: json.code || 0,
|
|
111
|
+
statusCode: response.status,
|
|
112
|
+
message: "Api request error",
|
|
113
|
+
headers,
|
|
114
|
+
data: json
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {body: json, response};
|
|
119
|
+
} catch(e: any) {
|
|
120
|
+
|
|
121
|
+
// Network error
|
|
122
|
+
if(e instanceof TypeError) {
|
|
123
|
+
if(isBrowser()) {
|
|
124
|
+
commit('SET_NOTIFICATION', {
|
|
125
|
+
type: "danger",
|
|
126
|
+
text: "Check your internet connection. No contact)"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Api Error instance
|
|
132
|
+
if(e instanceof ApiError) {
|
|
133
|
+
const statusCode = e.getStatusCode();
|
|
134
|
+
|
|
135
|
+
// Error notification
|
|
136
|
+
if(statusCode === 400 && options?.error) {
|
|
137
|
+
commit('SET_NOTIFICATION', {type: "danger", text: e.getData()["error"]});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if(statusCode === 404 && options?.error) {
|
|
141
|
+
if(isBrowser()) {
|
|
142
|
+
commit('SET_NOTIFICATION', {
|
|
143
|
+
type: "danger",
|
|
144
|
+
text: "Something went wrong. We are working hard to fix this)"
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
await dispatch('redirect', {
|
|
148
|
+
url: "/404",
|
|
149
|
+
code: 307
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Redirect (if response code 410)
|
|
155
|
+
if(options?.method?.toLowerCase() === "get") {
|
|
156
|
+
if(statusCode === 410) {
|
|
157
|
+
const location = e.getHeaders()?.location;
|
|
158
|
+
|
|
159
|
+
if(location) {
|
|
160
|
+
await dispatch("redirect", {url: location, code: 307});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw e;
|
|
167
|
+
} finally {
|
|
168
|
+
|
|
169
|
+
if(options?.preloader) {
|
|
170
|
+
commit('SET_LOADING', false);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createStore } from "vuex";
|
|
2
|
+
import {langLink} from "@/helpers/langLink";
|
|
3
|
+
import {fetchAction} from "./fetch";
|
|
4
|
+
|
|
5
|
+
import type {Context} from "vite-ssr-vue";
|
|
6
|
+
import type {HeadObject} from "@vueuse/head";
|
|
7
|
+
import type {Options, Breadcrumbs, Notification, FetchPayload} from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lang values
|
|
11
|
+
*/
|
|
12
|
+
type Lang = "ru"|"ua";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base store structure
|
|
16
|
+
*/
|
|
17
|
+
export interface StateBase {
|
|
18
|
+
|
|
19
|
+
// Ssr context
|
|
20
|
+
context: Context,
|
|
21
|
+
|
|
22
|
+
// Head Object
|
|
23
|
+
page: HeadObject,
|
|
24
|
+
|
|
25
|
+
// Breadcrumbs
|
|
26
|
+
way: Array<Breadcrumbs>,
|
|
27
|
+
|
|
28
|
+
// Before route - set to new value
|
|
29
|
+
toLang: Lang,
|
|
30
|
+
|
|
31
|
+
// After route set to new value
|
|
32
|
+
lang: Lang,
|
|
33
|
+
|
|
34
|
+
// is data loading
|
|
35
|
+
loading: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create store instance
|
|
40
|
+
* @param opt
|
|
41
|
+
*/
|
|
42
|
+
export default (opt: Options) => createStore({
|
|
43
|
+
state: () => {
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
context: {} as Context,
|
|
47
|
+
page: {},
|
|
48
|
+
way: [],
|
|
49
|
+
toLang: "ru",
|
|
50
|
+
lang: "ru",
|
|
51
|
+
loading: false,
|
|
52
|
+
} as StateBase
|
|
53
|
+
},
|
|
54
|
+
mutations: {
|
|
55
|
+
MERGE_CONTEXT(state, context) {
|
|
56
|
+
Object.assign(state.context, context);
|
|
57
|
+
},
|
|
58
|
+
MERGE_CONTEXT_HEADERS(state, headers: Record<string, any>) {
|
|
59
|
+
state.context.responseHeaders = Object.assign(state.context.responseHeaders || {}, headers);
|
|
60
|
+
},
|
|
61
|
+
SET_PAGE(state, page: HeadObject) {
|
|
62
|
+
state.page = page || {};
|
|
63
|
+
},
|
|
64
|
+
SET_WAY(state, way: Array<Breadcrumbs>) {
|
|
65
|
+
state.way = way || [];
|
|
66
|
+
},
|
|
67
|
+
SET_LANG(state, lang: Lang) {
|
|
68
|
+
state.lang = lang;
|
|
69
|
+
},
|
|
70
|
+
SET_TO_LANG(state, lang: Lang) {
|
|
71
|
+
state.toLang = lang;
|
|
72
|
+
},
|
|
73
|
+
SET_LOADING(state, loading: boolean) {
|
|
74
|
+
state.loading = loading
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
getters: {
|
|
78
|
+
getToLang(state): Lang {
|
|
79
|
+
|
|
80
|
+
return state.toLang;
|
|
81
|
+
},
|
|
82
|
+
getLang(state): Lang {
|
|
83
|
+
|
|
84
|
+
return state.lang;
|
|
85
|
+
},
|
|
86
|
+
urlFromLang: state => (url = '/', lang = state.toLang) => langLink(url, lang),
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get absolute url for requests
|
|
90
|
+
* @param state
|
|
91
|
+
*/
|
|
92
|
+
getBaseUrl(state): string {
|
|
93
|
+
|
|
94
|
+
return `${state.context.protocol}://${state.context.hostname}`;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
actions: {
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Base fetch data action
|
|
101
|
+
* @param state
|
|
102
|
+
* @param payload
|
|
103
|
+
* @returns
|
|
104
|
+
*/
|
|
105
|
+
async fetch(
|
|
106
|
+
state, payload: FetchPayload
|
|
107
|
+
): Promise<{body: Record<string, any>, response: Response}> {
|
|
108
|
+
|
|
109
|
+
return fetchAction(state, payload);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Redirect
|
|
114
|
+
* @param context
|
|
115
|
+
* @param url auto transform to multilang
|
|
116
|
+
* @param query query params
|
|
117
|
+
* @param code status code for server, when on the client is discarded
|
|
118
|
+
*/
|
|
119
|
+
async redirect(
|
|
120
|
+
{state, getters},
|
|
121
|
+
{url, code, query} : {url: string, code: 301|307, query?:Record<string, any>}
|
|
122
|
+
):Promise<void> {
|
|
123
|
+
|
|
124
|
+
if(query) {
|
|
125
|
+
url = url + new URLSearchParams(query as Record<string, any>).toString();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await opt.redirect(langLink(url, state.toLang), code || 307);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch payload
|
|
3
|
+
*/
|
|
4
|
+
export declare type FetchPayload = [
|
|
5
|
+
params:Record<string, any> | FormData,
|
|
6
|
+
options?:FetchOptions
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Addition fetch options
|
|
11
|
+
*/
|
|
12
|
+
export interface FetchOptions {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* relative url string
|
|
16
|
+
*/
|
|
17
|
+
url?: string,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* request method
|
|
21
|
+
*/
|
|
22
|
+
method?: string,
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* is need preloader
|
|
26
|
+
*/
|
|
27
|
+
preloader?: boolean,
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* is need error notification
|
|
31
|
+
*/
|
|
32
|
+
error?: boolean,
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* if need compress
|
|
36
|
+
*/
|
|
37
|
+
compress?: boolean,
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* request headers
|
|
41
|
+
*/
|
|
42
|
+
headers?: Record<string, string>,
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Request body|FormData
|
|
46
|
+
*/
|
|
47
|
+
body?:string|FormData,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* mege headers to default
|
|
51
|
+
*/
|
|
52
|
+
mergeHeaders?:Record<string, any>,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* any addition params
|
|
56
|
+
*/
|
|
57
|
+
[key:string]:any
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Notification object
|
|
62
|
+
*/
|
|
63
|
+
export interface Notification {
|
|
64
|
+
type: string|null,
|
|
65
|
+
text: string|null,
|
|
66
|
+
[key: string]: any
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create store options
|
|
71
|
+
*/
|
|
72
|
+
export interface Options {
|
|
73
|
+
redirect: (url: string, statusCode: 301|307) => Promise<void>,
|
|
74
|
+
[key:string]:any
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Breadcrumbs object
|
|
79
|
+
*/
|
|
80
|
+
export interface Breadcrumbs {
|
|
81
|
+
test: string,
|
|
82
|
+
to: string|null|undefined
|
|
83
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="error-page">
|
|
3
|
+
<div class="code">{{code}}</div>
|
|
4
|
+
<div class="content">Oops. Something went wrong</div>
|
|
5
|
+
<RouterLink
|
|
6
|
+
class="go-home"
|
|
7
|
+
to="/"
|
|
8
|
+
underline
|
|
9
|
+
>Go home</RouterLink>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import {defineComponent} from "vue";
|
|
15
|
+
import {mapMutations} from "vuex";
|
|
16
|
+
|
|
17
|
+
export default defineComponent({
|
|
18
|
+
name: "Error",
|
|
19
|
+
async prefetch({store, isClient}, to, from) {
|
|
20
|
+
store.commit('SET_PAGE', {
|
|
21
|
+
title: "Error page. Something went wrong",
|
|
22
|
+
htmlAttrs: {
|
|
23
|
+
lang: "en"
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
store.commit("MERGE_CONTEXT", {statusCode: 404});
|
|
27
|
+
},
|
|
28
|
+
props: {
|
|
29
|
+
code: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: () => "404"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
created() {
|
|
35
|
+
this.mergeContext({statusCode: this.code})
|
|
36
|
+
},
|
|
37
|
+
methods: {
|
|
38
|
+
...mapMutations({
|
|
39
|
+
mergeContext: 'MERGE_CONTEXT'
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style lang="postcss">
|
|
46
|
+
.error-page {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-basis: 100%;
|
|
49
|
+
align-items: center;
|
|
50
|
+
flex-flow: column wrap;
|
|
51
|
+
.code {
|
|
52
|
+
font-weight: 400;
|
|
53
|
+
font-size: 11rem;
|
|
54
|
+
}
|
|
55
|
+
.content {
|
|
56
|
+
font-size: 1.2rem;
|
|
57
|
+
}
|
|
58
|
+
.go-home {
|
|
59
|
+
font-weight: bold;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<h1 class="index">
|
|
3
|
+
{{page.h1}}
|
|
4
|
+
</h1>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import {defineComponent} from "vue";
|
|
9
|
+
import {mapState} from "vuex";
|
|
10
|
+
import {customerPrefetch} from "@/frontend/helpers/customerPrefetch";
|
|
11
|
+
|
|
12
|
+
export default defineComponent({
|
|
13
|
+
name: "Index",
|
|
14
|
+
components: {
|
|
15
|
+
},
|
|
16
|
+
prefetch: customerPrefetch({controller: "Index"}),
|
|
17
|
+
computed: {
|
|
18
|
+
...mapState(["page", "context"]),
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style lang="postcss">
|
|
24
|
+
.index {
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { createRouter, createWebHistory } from "vue-router";
|
|
3
|
+
import Index from "../Index.vue"
|
|
4
|
+
import {createStore} from "vuex";
|
|
5
|
+
|
|
6
|
+
// https://next.vue-test-utils.vuejs.org/guide/
|
|
7
|
+
describe("Index", () => {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createWebHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{path: "/", component: {}}
|
|
12
|
+
]
|
|
13
|
+
});
|
|
14
|
+
const store = createStore({
|
|
15
|
+
state: {
|
|
16
|
+
page: {
|
|
17
|
+
h1: "Test Page"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("should display header text", async() => {
|
|
23
|
+
const wrapper = mount(Index, {
|
|
24
|
+
global: {
|
|
25
|
+
plugins: [router, store]
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(wrapper.html()).toContain("Test Page")
|
|
30
|
+
})
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For link by lang
|
|
3
|
+
* @param url
|
|
4
|
+
* @param lang
|
|
5
|
+
* @param defaultLang
|
|
6
|
+
* @returns lang-link
|
|
7
|
+
*/
|
|
8
|
+
export const langLink = (
|
|
9
|
+
url: string,
|
|
10
|
+
lang: string = "ru",
|
|
11
|
+
defaultLang: string = "ru"
|
|
12
|
+
): string => {
|
|
13
|
+
url = (url || "").replace(/\/(ru|ua|en)(\/|$)/, '/');
|
|
14
|
+
const result = (lang === defaultLang ? '' : ('/' + lang)) + url;
|
|
15
|
+
|
|
16
|
+
return result === '/' ? result : result.replace(/\/$/, '');
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse current lang from url
|
|
3
|
+
* @param url
|
|
4
|
+
* @param defaultLang
|
|
5
|
+
*/
|
|
6
|
+
export const parseLang = (url: string, defaultLang = "ru") => {
|
|
7
|
+
const result = url.match(/^\/(ru|ua|en)\/?/i);
|
|
8
|
+
|
|
9
|
+
return result && result.length > 0 ? result[1] : defaultLang;
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {createServer, ViteDevServer} from "vite";
|
|
2
|
+
import fetch from "cross-fetch";
|
|
3
|
+
import {apiUrl} from "./jest.setup";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
var server: ViteDevServer;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default async(config: Record<string, any>) => {
|
|
10
|
+
global.server = await createServer();
|
|
11
|
+
await global.server.listen();
|
|
12
|
+
await new Promise(resolve => {
|
|
13
|
+
const interval = setInterval(async() => {
|
|
14
|
+
try {
|
|
15
|
+
await fetch(apiUrl);
|
|
16
|
+
clearInterval(interval);
|
|
17
|
+
resolve(true);
|
|
18
|
+
} catch(e) {}
|
|
19
|
+
|
|
20
|
+
}, 50);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import crossFetch from "cross-fetch";
|
|
2
|
+
import connect from "../../connect.json"
|
|
3
|
+
|
|
4
|
+
const apiUrl = `http://${connect.server.development.host}:${connect.server.development.vite.port}`;
|
|
5
|
+
const defaultHeaders = {
|
|
6
|
+
headers: {
|
|
7
|
+
"user-agent": "fetch-tests",
|
|
8
|
+
"content-type": "application/json"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const fetch = async(
|
|
12
|
+
input: RequestInfo,
|
|
13
|
+
init: RequestInit = {}
|
|
14
|
+
) => crossFetch(input, Object.assign({}, defaultHeaders, init));
|
|
15
|
+
|
|
16
|
+
export {fetch, apiUrl};
|