Browse Source

Vasile

pull/2415/head
VasileKarpa 6 months ago
parent
commit
13b9763054
17 changed files with 1261 additions and 8 deletions
  1. 6
      backend/config/settings/base.py
  2. 1
      doccano
  3. 540
      frontend/assets/css/fonts.css
  4. BIN
      frontend/assets/settings-icon.png
  5. BIN
      frontend/assets/settings-icon.png.png
  6. 15
      frontend/components/layout/TheHeader.vue
  7. 100
      frontend/components/settings/FormCreate.vue
  8. 38
      frontend/components/settings/FormDelete.vue
  9. 105
      frontend/components/users/FormCreate.vue
  10. 143
      frontend/components/users/UsersList.vue
  11. 119
      frontend/pages/settings/index.vue
  12. 76
      frontend/pages/settings/users.vue
  13. 2
      frontend/plugins/repositories.ts
  14. 32
      frontend/repositories/BaseRepository.js
  15. 23
      frontend/repositories/settings.js
  16. 26
      frontend/repositories/user/apiUserRepository.ts
  17. 43
      frontend/services/user.service.ts

6
backend/config/settings/base.py

@ -112,7 +112,7 @@ TEMPLATES = [
STATIC_URL = "/static/"
STATIC_ROOT = path.join(BASE_DIR, "staticfiles")
STATICFILES_DIRS = [
path.join(BASE_DIR, "client/dist/static"),
# path.join(BASE_DIR, "client/dist/static"),
]
# STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
@ -226,14 +226,14 @@ if DATABASES["default"].get("ENGINE") == "sql_server.pyodbc":
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", False)
CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", False)
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", [])
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", ["http://127.0.0.1:8080", "http://172.20.10.3:3000", "http://192.168.1.78:3000"])
# Allow all host headers
ALLOWED_HOSTS = ["*"]
if DEBUG:
CORS_ORIGIN_ALLOW_ALL = True
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:3000", "http://0.0.0.0:3000", "http://localhost:3000"]
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:3000", "http://0.0.0.0:3000", "http://localhost:3000", "http://127.0.0.1:8080", "http://172.20.10.3:3000", "http://192.168.1.78:3000"]
CSRF_TRUSTED_ORIGINS += env.list("CSRF_TRUSTED_ORIGINS", [])
# Batch size for importing data

1
doccano

@ -0,0 +1 @@
Subproject commit a485e324233876de42fa32b2d838c175d343beff

540
frontend/assets/css/fonts.css

@ -0,0 +1,540 @@
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-cyrillic-ext1.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-cyrillic2.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-greek-ext3.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-greek4.woff2') format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-math5.woff2') format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-symbols6.woff2') format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-vietnamese7.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-latin-ext8.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-100-latin9.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-cyrillic-ext10.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-cyrillic11.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-greek-ext12.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-greek13.woff2') format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-math14.woff2') format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-symbols15.woff2') format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-vietnamese16.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-latin-ext17.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-300-latin18.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-cyrillic-ext19.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-cyrillic20.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-greek-ext21.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-greek22.woff2') format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-math23.woff2') format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-symbols24.woff2') format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-vietnamese25.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-latin-ext26.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-400-latin27.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-cyrillic-ext28.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-cyrillic29.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-greek-ext30.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-greek31.woff2') format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-math32.woff2') format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-symbols33.woff2') format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-vietnamese34.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-latin-ext35.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-500-latin36.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-cyrillic-ext37.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-cyrillic38.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-greek-ext39.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-greek40.woff2') format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-math41.woff2') format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-symbols42.woff2') format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-vietnamese43.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-latin-ext44.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-700-latin45.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-cyrillic-ext46.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-cyrillic47.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-greek-ext48.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-greek49.woff2') format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-math50.woff2') format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-symbols51.woff2') format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-vietnamese52.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-latin-ext53.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-stretch: 100%;
font-display: swap;
src: url('~assets/fonts/Roboto-900-latin54.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

BIN
frontend/assets/settings-icon.png

Before After
Width: 96  |  Height: 96  |  Size: 2.4 KiB

BIN
frontend/assets/settings-icon.png.png

15
frontend/components/layout/TheHeader.vue

@ -62,6 +62,16 @@
<v-switch :input-value="isRTL" :label="direction" class="ms-1" @change="toggleRTL" />
</v-list-item-content>
</v-list-item>
<v-list-item @click="$router.push(localePath('/settings'))">
<v-list-item-icon>
<v-icon>{{ mdiAccountCog }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ $t('Settings') }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item @click="signout">
<v-list-item-icon>
<v-icon>{{ mdiLogout }}</v-icon>
@ -78,7 +88,7 @@
</template>
<script>
import { mdiLogout, mdiDotsVertical, mdiMenuDown, mdiHexagonMultiple } from '@mdi/js'
import { mdiLogout, mdiDotsVertical, mdiMenuDown, mdiHexagonMultiple, mdiAccountCog} from '@mdi/js'
import { mapGetters, mapActions } from 'vuex'
import TheColorModeSwitcher from './TheColorModeSwitcher'
import LocaleMenu from './LocaleMenu'
@ -109,7 +119,8 @@ export default {
mdiLogout,
mdiDotsVertical,
mdiMenuDown,
mdiHexagonMultiple
mdiHexagonMultiple,
mdiAccountCog
}
},

100
frontend/components/settings/FormCreate.vue

@ -0,0 +1,100 @@
<template>
<v-card>
<v-card-title>{{ $t('generic.create') }}</v-card-title>
<v-card-text>
<v-form v-model="valid" ref="form">
<v-text-field
v-model="username"
:label="$t('Username')"
:rules="usernameRules"
required
/>
<v-text-field
v-model="email"
:label="$t('Email')"
:rules="emailRules"
type="email"
required
/>
<v-text-field
v-model="password1"
:label="$t('Password')"
:rules="passwordRules"
type="password"
required
/>
<v-text-field
v-model="password2"
:label="$t('Confirm Password')"
:rules="[v => v === password1 || $t('user.passwordNotMatch')]"
type="password"
required
/>
</v-form>
<v-alert v-if="errorMessage" type="error" dense>{{ errorMessage }}</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="$emit('cancel')">{{ $t('generic.cancel') }}</v-btn>
<v-btn
color="primary"
:disabled="!valid"
:loading="loading"
@click="create"
>{{ $t('generic.save') }}</v-btn>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
valid: false,
loading: false,
username: '',
email: '',
password1: '',
password2: '',
errorMessage: '',
usernameRules: [
(v: string) => !!v || this.$t('user.usernameRequired'),
(v: string) => v.length <= 30 || this.$t('user.usernameTooLong')
],
emailRules: [
(v: string) => !!v || this.$t('user.emailRequired'),
(v: string) => /.+@.+\..+/.test(v) || this.$t('user.emailInvalid')
],
passwordRules: [
(v: string) => !!v || this.$t('user.passwordRequired'),
(v: string) => v.length >= 8 || this.$t('user.passwordTooShort')
]
}
},
methods: {
async create() {
if (!(this.$refs.form as Vue & { validate: () => boolean }).validate()) {
return
}
this.loading = true
this.errorMessage = ''
try {
await this.$services.user.create({
username: this.username,
email: this.email,
password1: this.password1,
password2: this.password2
})
this.$emit('save')
} catch (e) {
this.errorMessage = e.response.data.detail || this.$t('generic.error')
} finally {
this.loading = false
}
}
}
})
</script>

38
frontend/components/settings/FormDelete.vue

@ -0,0 +1,38 @@
<template>
<base-card
title="Delete Settings"
:agree-text="$t('generic.yes')"
:cancel-text="$t('generic.cancel')"
@agree="$emit('remove')"
@cancel="$emit('cancel')"
>
<template #content>
Are you sure you want to delete the following settings?
<v-list dense>
<v-list-item v-for="(item, i) in selected" :key="i">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</template>
</base-card>
</template>
<script>
import Vue from 'vue'
import BaseCard from '@/components/utils/BaseCard.vue'
export default Vue.extend({
components: {
BaseCard
},
props: {
selected: {
type: Array,
default: () => []
}
}
})
</script>

105
frontend/components/users/FormCreate.vue

@ -0,0 +1,105 @@
<template>
<v-dialog v-model="dialog" max-width="500px">
<v-card>
<v-card-title>{{ $t('generic.create') }} {{ $t('user.user') }}</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-text-field
v-model="form.username"
:label="$t('user.username')"
:rules="[rules.required]"
required
/>
<v-text-field
v-model="form.email"
:label="$t('user.email')"
:rules="[rules.required, rules.email]"
type="email"
required
/>
<v-text-field
v-model="form.password1"
:label="$t('user.password')"
:rules="[rules.required, rules.min]"
type="password"
required
/>
<v-text-field
v-model="form.password2"
:label="$t('user.confirmPassword')"
:rules="[rules.required, rules.match]"
type="password"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="close">{{ $t('generic.cancel') }}</v-btn>
<v-btn
:disabled="!valid"
color="primary"
@click="submit"
>
{{ $t('generic.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
dialog: false,
valid: false,
form: {
username: '',
email: '',
password1: '',
password2: ''
},
rules: {
required: (v: string) => !!v || this.$t('rules.required'),
email: (v: string) => /.+@.+\..+/.test(v) || this.$t('rules.email'),
min: (v: string) => v.length >= 8 || this.$t('rules.min', { min: 8 }),
match: (v: string) => v === this.form.password1 || this.$t('rules.passwordMatch')
}
}
},
methods: {
open() {
this.dialog = true
},
close() {
this.dialog = false
this.form = {
username: '',
email: '',
password1: '',
password2: ''
}
if (this.$refs.form) {
;(this.$refs.form as any).reset()
}
},
async submit() {
if (!(this.$refs.form as any).validate()) return
try {
await this.$services.auth.register(this.form)
this.close()
this.$emit('created')
} catch (e) {
console.error(e)
}
}
}
})
</script>

143
frontend/components/users/UsersList.vue

@ -0,0 +1,143 @@
<template>
<v-data-table
:value="value"
:headers="headers"
:items="items"
:options.sync="options"
:server-items-length="total"
:search="search"
:loading="isLoading"
:loading-text="$t('generic.loading')"
:no-data-text="$t('vuetify.noDataAvailable')"
:footer-props="{
showFirstLastPage: true,
'items-per-page-options': [10, 50, 100],
'items-per-page-text': $t('vuetify.itemsPerPageText'),
'page-text': $t('dataset.pageText')
}"
item-key="id"
show-select
@input="$emit('input', $event)"
>
<template #top>
<v-text-field
v-model="search"
:prepend-inner-icon="mdiMagnify"
:label="$t('generic.search')"
single-line
hide-details
filled
/>
</template>
<template #[`item.name`]="{ item }">
<nuxt-link :to="localePath(`/projects/${item.id}`)">
<span>{{ item.name }}</span>
</nuxt-link>
</template>
<template #[`item.createdAt`]="{ item }">
<span>{{
dateFormat(dateParse(item.createdAt, 'YYYY-MM-DDTHH:mm:ss'), 'YYYY/MM/DD HH:mm')
}}</span>
</template>
<template #[`item.tags`]="{ item }">
<v-chip v-for="tag in item.tags" :key="tag.id" outlined v-text="tag.text" />
</template>
</v-data-table>
</template>
<script lang="ts">
import { mdiMagnify } from '@mdi/js'
import { dateFormat } from '@vuejs-community/vue-filter-date-format'
import { dateParse } from '@vuejs-community/vue-filter-date-parse'
import type { PropType } from 'vue'
import Vue from 'vue'
import { DataOptions } from 'vuetify/types'
import { Project } from '~/domain/models/project/project'
export default Vue.extend({
props: {
isLoading: {
type: Boolean,
default: false,
required: true
},
items: {
type: Array as PropType<Project[]>,
default: () => [],
required: true
},
value: {
type: Array as PropType<Project[]>,
default: () => [],
required: true
},
total: {
type: Number,
default: 0,
required: true
}
},
data() {
return {
search: this.$route.query.q,
options: {} as DataOptions,
mdiMagnify,
dateFormat,
dateParse
}
},
computed: {
headers(): { text: any; value: string; sortable?: boolean }[] {
return [
{ text: this.$t('generic.name'), value: 'name' },
{ text: this.$t('generic.description'), value: 'description', sortable: false },
{ text: this.$t('generic.type'), value: 'projectType' },
{ text: 'Created', value: 'createdAt' },
{ text: 'Author', value: 'author' },
{ text: 'Tags', value: 'tags', sortable: false }
]
}
},
watch: {
options: {
handler() {
this.updateQuery({
query: {
limit: this.options.itemsPerPage.toString(),
offset: ((this.options.page - 1) * this.options.itemsPerPage).toString(),
q: this.search
}
})
},
deep: true
},
search() {
this.updateQuery({
query: {
limit: this.options.itemsPerPage.toString(),
offset: '0',
q: this.search
}
})
this.options.page = 1
}
},
methods: {
updateQuery(payload: any) {
const { sortBy, sortDesc } = this.options
if (sortBy.length === 1 && sortDesc.length === 1) {
payload.query.sortBy = sortBy[0]
payload.query.sortDesc = sortDesc[0]
} else {
payload.query.sortBy = 'createdAt'
payload.query.sortDesc = true
}
this.$emit('update:query', payload)
}
}
})
</script>

119
frontend/pages/settings/index.vue

@ -0,0 +1,119 @@
<template>
<v-card>
<v-card-title v-if="isStaff">
<v-btn class="text-capitalize" color="primary" @click.stop="dialogCreate = true">
{{ $t('generic.create') }}
</v-btn>
<v-dialog v-model="dialogCreate">
<form-create @cancel="dialogCreate = false" @save="onSave" />
</v-dialog>
<v-btn
class="text-capitalize ms-2"
:disabled="!canDelete"
outlined
@click.stop="dialogDelete = true"
>
{{ $t('generic.delete') }}
</v-btn>
<v-dialog v-model="dialogDelete">
<form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" />
</v-dialog>
</v-card-title>
<project-list
v-model="selected"
:items="projects.items"
:is-loading="isLoading"
:total="projects.count"
@update:query="updateQuery"
/>
</v-card>
</template>
<script lang="ts">
import _ from 'lodash'
import Vue from 'vue'
import { mapGetters } from 'vuex'
import ProjectList from '@/components/project/ProjectList.vue'
import FormDelete from '~/components/project/FormDelete.vue'
import FormCreate from '~/components/settings/FormCreate.vue'
import { Page } from '~/domain/models/page'
import { Project } from '~/domain/models/project/project'
import { SearchQueryData } from '~/services/application/project/projectApplicationService'
export default Vue.extend({
components: {
FormDelete,
FormCreate,
ProjectList
},
layout: 'projects',
middleware: ['check-auth', 'auth'],
data() {
return {
dialogCreate: false,
dialogDelete: false,
projects: {} as Page<Project>,
selected: [] as Project[],
isLoading: false
}
},
async fetch() {
this.isLoading = true
this.projects = await this.$services.project.list(
this.$route.query as unknown as SearchQueryData
)
this.isLoading = false
},
computed: {
...mapGetters('auth', ['isStaff']),
canDelete(): boolean {
return this.selected.length > 0
},
canClone(): boolean {
return this.selected.length === 1
}
},
watch: {
'$route.query': _.debounce(function () {
// @ts-ignore
this.$fetch()
}, 1000)
},
methods: {
async remove() {
await this.$services.project.bulkDelete(this.selected)
this.$fetch()
this.dialogDelete = false
this.selected = []
},
async clone() {
const project = await this.$services.project.clone(this.selected[0])
this.selected = []
this.$router.push(`/projects/${project.id}/settings`)
},
updateQuery(query: object) {
this.$router.push(query)
},
onSave() {
this.dialogCreate = false
this.$fetch()
}
}
})
</script>
<style scoped>
::v-deep .v-dialog {
width: 800px;
}
</style>

76
frontend/pages/settings/users.vue

@ -0,0 +1,76 @@
<template>
<v-card>
<v-card-title>
<v-btn class="text-capitalize" color="primary" @click.stop="dialogCreate = true">
{{ $t('generic.create') }}
</v-btn>
</v-card-title>
<users-list
v-model="selected"
:items="users.items"
:is-loading="isLoading"
:total="users.count"
@update:query="updateQuery"
/>
<v-dialog v-model="dialogCreate">
<form-create @created="handleUserCreated" />
</v-dialog>
</v-card>
</template>
<script lang="ts">
import _ from 'lodash'
import Vue from 'vue'
import FormCreate from '@/components/users/FormCreate.vue'
import UsersList from '@/components/users/UsersList.vue'
import { Page } from '~/domain/models/page'
import { User } from '~/domain/models/user'
export default Vue.extend({
components: {
FormCreate,
UsersList
},
layout: 'settings',
middleware: ['check-auth', 'auth'],
data() {
return {
users: {} as Page<User>,
selected: [] as User[],
isLoading: false,
dialogCreate: false
}
},
async fetch() {
await this.fetchUsers()
},
watch: {
'$route.query': _.debounce(function () {
// @ts-ignore
this.$fetch()
}, 1000)
},
methods: {
async fetchUsers() {
this.isLoading = true
this.users = await this.$services.user.list(this.$route.query)
this.isLoading = false
},
updateQuery(query: object) {
this.$router.push({ query })
},
handleUserCreated() {
this.dialogCreate = false
this.fetchUsers()
}
}
})
</script>

2
frontend/plugins/repositories.ts

@ -125,4 +125,4 @@ const plugin: Plugin = (_, inject) => {
}
export default plugin
export { repositories }
export { repositories }

32
frontend/repositories/BaseRepository.js

@ -0,0 +1,32 @@
import apiService from '@/services/api.service'
export class BaseRepository {
constructor(resource) {
this.resource = resource
}
get(path, params = {}) {
return apiService.get(`${this.resource}${path}`, { params })
.then(response => response.data)
}
post(path, data = {}) {
return apiService.post(`${this.resource}${path}`, data)
.then(response => response.data)
}
put(path, data = {}) {
return apiService.put(`${this.resource}${path}`, data)
.then(response => response.data)
}
patch(path, data = {}) {
return apiService.patch(`${this.resource}${path}`, data)
.then(response => response.data)
}
delete(path, params = {}) {
return apiService.delete(`${this.resource}${path}`, params)
.then(response => response.data)
}
}

23
frontend/repositories/settings.js

@ -0,0 +1,23 @@
import { BaseRepository } from './BaseRepository'
export class SettingsRepository extends BaseRepository {
constructor() {
super('settings')
}
async fetchAll() {
return await this.get('')
}
async create(params) {
return await this.post('', params)
}
async clone(params) {
return await this.post('/clone', params)
}
async delete(params) {
return await this.delete('', { data: params })
}
}

26
frontend/repositories/user/apiUserRepository.ts

@ -1,3 +1,4 @@
import { Page } from '@/domain/models/page'
import { UserItem } from '@/domain/models/user/user'
import ApiService from '@/services/api.service'
@ -5,6 +6,15 @@ function toModel(item: { [key: string]: any }): UserItem {
return new UserItem(item.id, item.username, item.is_superuser, item.is_staff)
}
function toPayload(item: { [key: string]: any }): { [key: string]: any } {
return {
username: item.username,
email: item.email,
password1: item.password1,
password2: item.password2
}
}
export class APIUserRepository {
constructor(private readonly request = ApiService) {}
@ -14,9 +24,21 @@ export class APIUserRepository {
return toModel(response.data)
}
async list(query: string): Promise<UserItem[]> {
async list(query: string): Promise<Page<UserItem>> {
const url = `/users?q=${query}`
const response = await this.request.get(url)
return response.data.map((item: { [key: string]: any }) => toModel(item))
return new Page(
response.data.count,
response.data.next,
response.data.previous,
response.data.results.map((item: { [key: string]: any }) => toModel(item))
)
}
async create(fields: { [key: string]: any }): Promise<UserItem> {
const url = '/users/create'
const payload = toPayload(fields)
const response = await this.request.post(url, payload)
return toModel(response.data)
}
}

43
frontend/services/user.service.ts

@ -0,0 +1,43 @@
import { Page } from '~/domain/models/page'
import { UserItem } from '~/domain/models/user/user'
import { APIUserRepository } from '~/repositories/user/apiUserRepository'
type UserFields = {
username: string
email: string
password1: string
password2: string
}
export interface SearchQueryData {
limit: string
offset: string
q?: string
}
export class UserApplicationService {
constructor(private readonly repository: APIUserRepository) {}
public async list(q: SearchQueryData): Promise<Page<UserItem>> {
try {
return await this.repository.list(q.q || '')
} catch (e: any) {
throw new Error(e.response.data.detail)
}
}
public async create(fields: UserFields): Promise<UserItem> {
try {
return await this.repository.create(fields)
} catch (e: any) {
throw new Error(e.response.data.detail)
}
}
public async getProfile(): Promise<UserItem> {
try {
return await this.repository.getProfile()
} catch (e: any) {
throw new Error(e.response.data.detail)
}
}
Loading…
Cancel
Save