Init project

This commit is contained in:
schooltechx 2023-03-14 13:47:44 +07:00
parent 050fdb4f64
commit e5d6c890a8
46 changed files with 7856 additions and 0 deletions

55
.github/workflows/build.yaml vendored Normal file
View file

@ -0,0 +1,55 @@
name: build-docker
run-name: build-docker ${{ github.actor }}
on:
push:
tags:
- v1.**
branches:
- 'main'
# Allow run workflow manually from Action tab
workflow_dispatch:
env:
REGISTRY: docker.frappet.com
CMS_IMAGE_NAME: demo/qualifying-exam-cms
CMS_IMAGE_TAG: 0.1.0
jobs:
# act --workflows .github/workflows/build.yaml --job remote-image -s DOCKER_USER -s DOCKER_PASS
# act -W .github/workflows/build.yaml -j remote-image -s DOCKER_USER -s DOCKER_PASS
remote-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# skip Set up QEMU because it fail on act and container
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{env.REGISTRY}}
username: ${{secrets.DOCKER_USER}}
password: ${{secrets.DOCKER_PASS}}
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
context: cms
# platforms: linux/amd64,linux/arm64
push: true
tags: ${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:${{env.CMS_IMAGE_TAG}},${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:latest
# act -W .github/workflows/build.yaml -j local-image
local-image:
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
context: cms
load: true
tags: ${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:${{env.CMS_IMAGE_TAG}},${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:latest

34
README.md Normal file
View file

@ -0,0 +1,34 @@
# ระบบสรรคหา
แบ่งออกเป็นสามส่วน
- ส่วนจัดการการสอบแข่งขัน BE .NET,FE Vue.js อยู่ในระบบหลัก อยู่ในเน็ตเวิร์กของ BKK
- ส่วนรับสมัครสอบคัดเลือก FE/BE จะใช้คนละ Realms แยกจากระบบหบัก ผู้สมัครจะใช้ email ในการ login อยู่นอกเน็ตเวิร์กของ BKK
- เวปประกาศข่าว FE/BE Sveltekit ควรใช้การดึงข้อมูลจาก สอบแข่งขัน สอบคัดเลือก ผ่าน API ไม่ต้อง Authentication เพราะเป็นสาธารณะอยู่แล้ว อยู่นอกเน็ตเวิร์กของ BKK
## ส่วนจัดการการสอบแข่งขัน
ทำส่วนนำเข้าข้อมูลไปบ้างแล้ว อยู่ที่ [BMA-EHR-RECRUIT-SERVICE](https://github.com/Frappet/BMA-EHR-RECRUIT-SERVICE)
ต้องมี API สำหรับดึงข้อมูลไปเวปประกาศข่าว
## ส่วนรับสมัครสอบคัดเลือก
การรับสมัครสอบคัดเลือกจำเป็นต้องมี
- Backoffice เจ้าหน้าที่ใน eHR สำหรับหน่วยงานสร้างรอบ, เจ้าหน้าที่นำเข้าข้อมูล, ก.ก. อนุมัติ
- Frontend หน้าเวปสำหรับผู้ใช้งานมาสมัคร
- API สำหรับดึงข้อมูลไปเวปประกาศข่าว
## เวปประกาศข่าว
หน้าเวปสำหรับผู้สมัครมาอ่านข่าวประกาศ
- Frontend ดึงข้อมูลจาก สอบแข่งขัน สอบคัดเลือก มาแสดง และข้อมูลทั่วไปของเวปเองจาก Backoffice
- Backoffice แบบหนึ่ง มี Admin control panel และฐานข้อมูลในตัวเอง login ในตัวเอง แยกจากระบบหลักเลย ใช้เข้าไปจัดการข้อมูลข่าวอื่นๆ และข้อมูลเวปได้ แบบนี้น่าจะงานหนักหน่อยเพราะต้องทำ authen เอง
- Backoffice แบบสอง ตัวจัดการข้อมูลอยู่ใน eHR ดึงข้อมูลผ่าน API มาแสดง ถ้าเลือกทำแบบนี้ CMS จะเป็นแบบอ่านอย่างเดียว ปลอดภัย Stateless อาจจะมองเรื่อง cache API ร่วมด้วย
## Info
ข้อมูลเพิ่มเติมนอกจาก TOR สามารถดูได้ที่เวป [กองสรรหาบุคคล (Bangkok careers) ](https://webportal.bangkok.go.th/KSB) มีหลัการ
[การสรรหาและเลือกสรร](https://webportal.bangkok.go.th/KSB/page/sub/19478/%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%AA%E0%B8%A3%E0%B8%A3%E0%B8%AB%E0%B8%B2%E0%B9%81%E0%B8%A5%E0%B8%B0%E0%B9%80%E0%B8%A5%E0%B8%B7%E0%B8%AD%E0%B8%81%E0%B8%AA%E0%B8%A3%E0%B8%A3)
## BKK CMS
ทางกรุงเทพมหานครมี [Web Portal](https://webportal.bangkok.go.th/) เป็นของตัวเองอยู่แล้ว ข้อมูลเยอะและซับซ้อน ควรเลือกดูเท่าที่จำเป็น ยังไม่แน่ใจว่าใครรับผิดชอบในการอัปเดตข่าวสาร มี่ข้อมูลจากหลายหน่วยงานอาจจะมี login ไปทำกันเอง มีลิงค์บางส่วนใช้งานไม่ได้ (อาจจะเป็นเวปภายใน)
- [โครงสร้างองค์กร](https://webportal.bangkok.go.th/KSB/page/sub/10761/%E0%B9%82%E0%B8%84%E0%B8%A3%E0%B8%87%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87%E0%B8%AD%E0%B8%87%E0%B8%84%E0%B9%8C%E0%B8%81%E0%B8%A3) สามารถหาข้อมูลเพิ่มเติมของแต่ละสำนัก หน่วยงานเขต และ โรงพยาบาลในสังกัด สนพ. ได้ เมื่อเข้าไปดูโครงสร้่างในแต่ละสำนักเป็นภาพไม่ได้สร้างจากฐานข้อมูล
- [เวปหนังสือเวียน](https://circular.bangkok.go.th/)ดูแลโดย สำนักยุทธศาสตร์และประเมินผล. เอกสาร PDF บางอันมาจากที่นี้แล้วเอาลิงค์ไปแปะใน Portal ทำหลายอย่าง หนังสือเวียน, มีการขอใช้อีเมลล์(Zimbra), Linkage, จองห้องประชุม, ขอ IP Address ทางบริษัทเราน่าจะต้องติดต่อหน่วยงาน
เอกสารเป็น client paging โหลดค่อนข้างช้าข้อมูลมีแนวโน้มจะเพิ่มขึ้นเรื่อยๆ(9000+)
- [สถาบันพัฒนาข้าราชการกรุงเทพมหานคร](https://webportal.bangkok.go.th/training)
- [กองสรรหาบุคคล (Bangkok careers) ](https://webportal.bangkok.go.th/KSB)

13
cms/.eslintignore Normal file
View file

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

20
cms/.eslintrc.cjs Normal file
View file

@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

10
cms/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
cms/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

13
cms/.prettierignore Normal file
View file

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

9
cms/.prettierrc Normal file
View file

@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

12
cms/Dockerfile Normal file
View file

@ -0,0 +1,12 @@
# docker build . -t registry.home.lan/demo/be2:latest
FROM node:18 as build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app .
EXPOSE 80
ENV PORT=80
CMD ["node", "./build/index.js"]

85
cms/README.md Normal file
View file

@ -0,0 +1,85 @@
# เวปประกาศข่าว (CMS)
การพัฒนาแบบที่เป็นอยู่ Vue(SPA)+dotnet(Web API) ของบริษัทไม่ได้รองรับ SEO เท่าใดนัก จำเป็นต้องใช้ Meta Framework ที่รองรับ SSR เช่น Nuxt.js เนื่องจากไม่ขึ้นกับส่วนอื่นมากนัก จะทดลองใช้เทคโนโลยีแบบใหม่ๆในการพัฒนา ที่ง่ายในการพัฒนากว่าเดิมจะ SvelteKit เป็นฐาน Daisy UI และ Daisy UI สำหรับการทำ Frontend และจะนำเครื่องมือในการ Automate ต่างๆมาร่วมด้วย
- [Sveltekit](https://www.youtube.com/watch?v=uEJ-Rnm2yOE) Meta JS Framework รองรับ SSR เขียนง่าย
- [TailWindCSS]() ติดตั้งด้วย [svelte-add](https://github.com/svelte-add/tailwindcss)
- [Daisy UI ](https://daisyui.com/)
- [PWA](https://web.dev/progressive-web-apps/) ทำให้ Web App เหมือนแอปมือถือ
- [Playwright](https://playwright.dev/) เขียนโค้ดทำการทดสอบเวปแอปบน Browser
- [Vitest](https://vitest.dev/) ทำ Unit test สำหรับ Vite
- [GitHub Actions](https://github.com/features/actions) ทำ CI/CD ในระบบทดสอบ
- [nektos/act](https://github.com/nektos/act) ใช้ GitHub Actions บนเครื่องของเราเอง
- [Argo CD](https://argo-cd.readthedocs.io/en/stable/) deploy ขึ้นระบบ Production (K8s)
- [Keycloak](https://www.keycloak.org/) ใช้สำหรับทำระบบ Login
## Install
คำสั่งสำหรับเริ่มต้นสร้างโปรเจ็กเปล่าๆ จนใช้งาน
``` bash
# เลือก Skeleton project,Typescript แล้ว ติดตั้งทั้งหมด
npm create svelte@latest cms-recruit
cd cms-recruit
npm install
# ติดตั้ง Tailwindcss พร้อมเซ็ตค่าให้เรียบร้อย
npx svelte-add@latest tailwindcss
# เมื่อติดตั้ง daisyui ให้ดูวิธีการตั้งค่าเพิ่มจากในเวป
npm i daisyui
# ใช้สำหรับทำเป็น Node.js ทำ Docker Image
npm i -D @sveltejs/adapter-node
# Keycloak สำหรับ frontend และ backend
npm i -D keycloak-js keycloak-backend
# ช่วยทำ seo ให้ง่ายขึ้น
npm install -D svelte-seo
```
## Sveltekit
คำสั่งที่ใช้
``` bash
npm run dev -- --port=4000 --host=0.0.0.0
npm run build
npm run preview -- --port=4000 --host=0.0.0.0
# หรือ
npx vite --port=4000 --host=0.0.0.0
npm vite build
npx vite preview --port=4000 --host=0.0.0.0
```
โครงสร้างโค้ด
- หน้าเวปอยู่ใน [src/routes/](src/routes/) โครงหน้าหลัก +layout.svelte,หน้าต่างๆ +page.svelte
- library/component ที่ใช้ร่วมกัน [src/lib/](src/lib/)
ตัวอย่างการใช้ API โค้ดอยู่ใน [src/routes/api/users](./src/routes/api/users/)
- http://localhost:4000/api/users
- http://localhost:4000/api/users/1
## Browser Testing
ใช้ Playwright มีคอนฟิกไฟล์ [playwright.config.ts](./playwright.config.ts) โค้ดสำหรับการทดสอบจะอยู่ใน โฟลเดอร์ [tests](./tests/) ใน VS Code จะติดตั้ง Playwright Extension รูปเป็นเหมือนโหลรูปชมพู่สำหรับทดลองวิทยาศาสตร์ ให้กดปุ่มเล่นเพื่อทำการทดสอบ
```bash
# ติดตั้ง embeded broser ของ playwright
npx playwright install chromium
# รันเทสที่อยู่ในโฟลเดอร์ tests
npm run test
```
### ดูเพิ่มเติมได้ที่
- [การทำ Browser Automation ด้วย Playwright](https://www.youtube.com/watch?v=Bc6dz4hs_r0)
## Unit Testing
ใช้ Vitest ทำ [Unit test](https://www.youtube.com/watch?v=5bQD3dCoyHA) คอนฟิกจะอยู่ที่ [vite.config.ts](./vite.config.ts) แนะนำให้ใส่ไว้ใน lib ในรูปแบบ src/lib/*.test.ts
แนะนำให้เขียนโค้ดใน
``` bash
npm run test:unit
```
# CI Github Action
ให้ดูโค้ดที่ ../.github/workflow
- [ตัวอย่างการใช้งานในบริษัท](https://youtu.be/k1w_cCzCd0o)
# CD Argo CD
TODO
# Note
## PWA
ใน [app.html](./src/app.html) จะเรียกใช้ [manifest.json](./src/static/manifest.json)
ไฟล์ [service-worker.ts](./src/service-worker.ts) จะถูก SvelteKit นำไปทำเป็น Service Worker ให้
- [ตอนนี้ใช้แบบง่ายตามนี้](https://thecodingchannel.hashnode.dev/turn-your-sveltekit-app-into-a-pwa-in-3-simple-steps)
# Keycloak
ยังไม่แน่ใจว่าการ Authen จำเป็นสำหรับ CMS หรือเปล่า เพราะใช้แค่แสดงข้อมูล อาจจะใช้หน้า Frontend ของ eHR ในการใส่ข้อมูลทั้งหมดก็น่าจะดีกว่า ตอนนี้ใช้ Repository pattern ไปก่อน
- Frontend ใช้ [Keycloak JS](https://www.npmjs.com/package/keycloak-js)
- API จะไม่ใช้ keycloak-connect เพราะมันรองรับเฉพาะ Express จะใช้ [keycloak-backend](https://www.npmjs.com/package/keycloak-backend) หรือตัวอื่นๆแทน
- ควรใช้การก็อปปี้ public key มาเก็บไว้ แทนการ verify token แบบ online จะเร็วกว่า [How to verify a JWT token in JavaScript and Node.js?](https://www.youtube.com/watch?v=gm2PBHyjQmM)

6932
cms/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

47
cms/package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "cms",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.2.2",
"@sveltejs/kit": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.7",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"keycloak-backend": "^4.0.1",
"postcss": "^8.4.14",
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"sass": "^1.58.3",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"svelte-preprocess": "^4.10.7",
"svelte-seo": "^1.5.3",
"tailwindcss": "^3.1.5",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0",
"vitest": "^0.25.3"
},
"type": "module",
"dependencies": {
"daisyui": "^2.51.3"
}
}

11
cms/playwright.config.ts Normal file
View file

@ -0,0 +1,11 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests'
};
export default config;

13
cms/postcss.config.cjs Normal file
View file

@ -0,0 +1,13 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;

12
cms/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

13
cms/src/app.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<link rel="manifest" href="manifest.json" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

4
cms/src/app.postcss Normal file
View file

@ -0,0 +1,4 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;

7
cms/src/index.test.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View file

@ -0,0 +1,33 @@
<div class="navbar bg-base-100">
<div class="navbar-start">
<div class="dropdown">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<label tabindex="0" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
</label>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul tabindex="0" class="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="/">หน้าหลัก</a></li>
<li><a href="/qualifying">การสอบคัดเลือก</a></li>
<li><a href="/competitive">การสอบแข่งขัน</a></li>
<li><a href="/about">เกี่ยวกับเรา</a></li>
</ul>
</div>
<a class="btn btn-ghost normal-case text-xl"
href="https://webportal.bangkok.go.th/KSB/page/top/1119/%E0%B8%81%E0%B8%AD%E0%B8%87%E0%B8%AA%E0%B8%A3%E0%B8%A3%E0%B8%AB%E0%B8%B2%E0%B8%9A%E0%B8%B8%E0%B8%84%E0%B8%84%E0%B8%A5">
กองสรรหา
</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a href="/">หน้าหลัก</a></li>
<li><a href="/qualifying">การสอบคัดเลือก</a></li>
<li><a href="/competitive">การสอบแข่งขัน</a></li>
<li><a href="/about">เกี่ยวกับเรา</a></li>
</ul>
</div>
<div class="navbar-end">
<a class="btn" href="/">เข้าสู่ระบบ</a>
</div>
</div>

View file

@ -0,0 +1,13 @@
{
"contact":{
"company_name": "กองสรรหาบุคคล",
"description":
"มีหน้าที่รับผิดชอบเกี่ยวกับการสรรหาและเลือกสรรบุคคลเข้ารับราบการเป็นข้าราขการกรุงเทพมหานคร สามัญและข้าราชการครูกรุงเทพมหานครเฉพาะสังกัดสำนักพัฒนาชุมชน ....",
"contact_email_address": "sale@frappet.com",
"phone": "(662) xxx xxx",
"address": "123 Example Street, xxx, 9876, yyy",
"zip": "10510",
"Country": "Thailand"
}
}

View file

@ -0,0 +1,8 @@
//TDD
import {it,expect } from 'vitest'
import {getContact} from './info'
it('test getContact() ',async ()=>{
const result = await getContact()
expect(result.company_name).toBe("กองสรรหาบุคคล")
expect(result.Country).toBe("Thailand")
})

4
cms/src/lib/data/info.ts Normal file
View file

@ -0,0 +1,4 @@
import info from "./info.json"
export async function getContact(){
return info.contact
}

View file

@ -0,0 +1,72 @@
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"phone": "1-770-736-8031 x56442"
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"phone": "010-692-6593 x09125"
},
{
"id": 3,
"name": "Clementine Bauch",
"username": "Samantha",
"email": "Nathan@yesenia.net",
"phone": "1-463-123-4447"
},
{
"id": 4,
"name": "Patricia Lebsack",
"username": "Karianne",
"email": "Julianne.OConner@kory.org",
"phone": "493-170-9623 x156"
},
{
"id": 5,
"name": "Chelsey Dietrich",
"username": "Kamren",
"email": "Lucio_Hettinger@annie.ca",
"phone": "(254)954-1289"
},
{
"id": 6,
"name": "Mrs. Dennis Schulist",
"username": "Leopoldo_Corkery",
"email": "Karley_Dach@jasper.info",
"phone": "1-477-935-8478 x6430"
},
{
"id": 7,
"name": "Kurtis Weissnat",
"username": "Elwyn.Skiles",
"email": "Telly.Hoeger@billy.biz",
"phone": "210.067.6132"
},
{
"id": 8,
"name": "Nicholas Runolfsdottir V",
"username": "Maxime_Nienow",
"email": "Sherwood@rosamond.me",
"phone": "586.493.6943 x140"
},
{
"id": 9,
"name": "Glenna Reichert",
"username": "Delphine",
"email": "Chaim_McDermott@dana.io",
"phone": "(775)976-6794 x41206"
},
{
"id": 10,
"name": "Clementina DuBuque",
"username": "Moriah.Stanton",
"email": "Rey.Padberg@karina.biz",
"phone": "024-648-3804"
}
]

28
cms/src/lib/data/users.ts Normal file
View file

@ -0,0 +1,28 @@
import users from "./users.json"
let userid =100
export interface User {
id: number;
email: string;
username: string;
name:string;
last_name: string;
phone:string
}
export async function getUser(id:number){
return users.find(u=>u.id===id)
}
export async function getUsers(){
return users
}
export async function createUser(u:User){
u.id= userid++
return users.push(u)
}
export async function updateUser(u:User){
const user = await getUser(u.id)
if(!user)
return user
user.phone = u.phone
return user
}

View file

@ -0,0 +1,9 @@
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.error?.message}</h1>
<div>
มีบางอย่างไม่ถูกต้องกรุณาติดต่อ 02-xxxxxxxx
</div>

View file

@ -0,0 +1,11 @@
<script>
import '../app.postcss';
import Header from '$lib/components/Header.svelte';
</script>
<Header />
<div class="p-10 max-w-4xl place-content-center ">
<div>
<slot />
</div>
</div>

View file

@ -0,0 +1,14 @@
<h1 class="text-4xl">กองสรรหาบุคคล (Recruitment Division)</h1>
<h2 class="text-2xl">ประกาศเกี่ยวกับการคัดเลือกบุคลากรกทม</h2>
<div>xxxx</div>
<div>xxxx</div>
<div>xxxx</div>
<div>xxxx</div>
<h2 class="text-2xl">ประกาศเกี่ยวกับการสอบแข่งขัน</h2>
<div>xxxx</div>
<div>xxxx</div>
<div>xxxx</div>
<div>xxxx</div>

View file

@ -0,0 +1,12 @@
import {getContact} from "$lib/data/info"
import type { PageServerLoad } from './$types'
/*
export const load: PageServerLoad = async () => {
return getContact()
}*/
export const load = (async () => {
return getContact()
}) satisfies PageServerLoad

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { PageData } from './$types'
export let data: PageData;
</script>
<h1 class="text-4xl">{data.company_name}</h1>
<div>{data.description}</div>

View file

@ -0,0 +1,12 @@
import type { RequestEvent, RequestHandler } from './$types'
import {json} from '@sveltejs/kit'
import { createUser, getUsers } from '$lib/data/users'
//api/users
export const POST: RequestHandler = async ({ request }: RequestEvent) => {
const data = await request.json()
return json(await createUser( data))
}
export const GET: RequestHandler = async () => {
return json(await getUsers())
}

View file

@ -0,0 +1,19 @@
import type { RequestEvent, RequestHandler } from './$types'
import {json} from '@sveltejs/kit'
import { getUser, updateUser } from '$lib/data/users'
//api/users/1
export const PUT: RequestHandler = async ({ request }: RequestEvent) => {
const o = await request.json()
if(!updateUser(o))
return json({message:"call updateUser fail "},{status:400})
return json({message:"Update Success"})
}
export const GET: RequestHandler = async ({params}: RequestEvent) => {
const id = Number(params.id)
const u = await getUser(id)
if(!u)
return json({message:`User ${id} not found`},{status:404})
return json(u)
}

View file

@ -0,0 +1,7 @@
<h1 class="text-4xl">การสอบแข่งขัน</h1>
fdsafda
fdsafda sdaf
sdaffsdasaf rfffffffffffffffff fffffffffffffffffffffffff ffffffffffff fgedfgfdgfdsg
fedsddddddddddddd ddddddddddddddd ddddddddd d d d d d d d d d d d d xxxx

View file

@ -0,0 +1,6 @@
<h1 class="text-4xl">การสอบคัดเลือก</h1>
<button class="btn btn-outline">Button</button>
<button class="btn btn-outline btn-primary">Button</button>
<button class="btn btn-outline btn-secondary">Button</button>

66
cms/src/service-worker.ts Normal file
View file

@ -0,0 +1,66 @@
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { build, files, version } from '$service-worker';
// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;
const ASSETS = [
...build, // the app itself
...files // everything in `static`
];
self.addEventListener('install', (event) => {
// Create a new cache and add all files to it
async function addFilesToCache() {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
}
event.waitUntil(addFilesToCache());
});
self.addEventListener('activate', (event) => {
// Remove previous cached data from disk
async function deleteOldCaches() {
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key);
}
}
event.waitUntil(deleteOldCaches());
});
self.addEventListener('fetch', (event) => {
// ignore POST requests etc
if (event.request.method !== 'GET') return;
async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);
// `build`/`files` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
return cache.match(event.request);
}
// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
const response = await fetch(event.request);
if (response.status === 200) {
cache.put(event.request, response.clone());
}
return response;
} catch {
return cache.match(event.request);
}
}
event.respondWith(respond());
});

BIN
cms/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
cms/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="410"
height="404"
viewBox="0 0 410 404"
fill="none"
version="1.1"
id="svg20"
sodipodi:docname="favicon.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1001"
id="namedview22"
showgrid="false"
inkscape:zoom="0.51361386"
inkscape:cx="-374.79518"
inkscape:cy="145.0506"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="g8"
inkscape:document-rotation="0"
inkscape:pagecheckerboard="0" />
<path
d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z"
fill="url(#paint0_linear)"
id="path2" />
<defs
id="defs18">
<linearGradient
id="paint0_linear"
x1="6.00017"
y1="32.9999"
x2="235"
y2="344"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#41D1FF"
id="stop6" />
<stop
offset="1"
stop-color="#BD34FE"
id="stop8" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="194.651"
y1="8.81818"
x2="236.076"
y2="292.989"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#FFEA83"
id="stop11" />
<stop
offset="0.0833333"
stop-color="#FFDD35"
id="stop13" />
<stop
offset="1"
stop-color="#FFA800"
id="stop15" />
</linearGradient>
</defs>
<path
d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z"
fill="url(#paint1_linear)"
id="path4" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="PWA">
<g
id="g8"
transform="matrix(0.15789659,0,0,0.15890333,54.892928,275.21638)">
<path
fill="#3d3d3d"
fill-opacity="1"
stroke-width="0.2"
stroke-linejoin="round"
d="m 1436.62,603.304 56.39,-142.599 h 162.82 L 1578.56,244.39 1675.2,5.28336e-4 1952,734.933 h -204.13 l -47.3,-131.629 z"
id="path2-1"
style="fill:#3e3e3e;fill-opacity:1" />
<path
fill="#5a0fc8"
fill-opacity="1"
stroke-width="0.2"
stroke-linejoin="round"
d="M 1262.47,734.935 1558.79,0.00156593 1362.34,0.0025425 1159.64,474.933 1015.5,0.00351906 H 864.499 L 709.731,474.933 600.585,258.517 501.812,562.819 602.096,734.935 h 193.331 l 139.857,-425.91 133.346,425.91 z"
id="path4-4"
style="fill:#2e859c;fill-opacity:1" />
<path
fill="#3d3d3d"
fill-opacity="1"
stroke-width="0.2"
stroke-linejoin="round"
d="m 186.476,482.643 h 121.003 c 36.654,0 69.293,-4.091 97.917,-12.273 l 31.293,-96.408 87.459,-269.446 C 517.484,93.9535 509.876,83.9667 501.324,74.5569 456.419,24.852 390.719,4.06265e-4 304.222,4.06265e-4 H -3.8147e-6 V 734.933 H 186.476 Z M 346.642,169.079 c 17.54,17.653 26.309,41.276 26.309,70.871 0,29.822 -7.713,53.474 -23.138,70.956 -16.91,19.425 -48.047,29.137 -93.409,29.137 H 186.476 V 142.598 h 70.442 c 42.277,0 72.185,8.827 89.724,26.481 z"
id="path6"
style="fill:#3e3e3e;fill-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

28
cms/static/manifest.json Normal file
View file

@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "BKK Recrute",
"short_name": "BKK Recrute",
"start_url": "/",
"display": "standalone",
"background_color": "#fff",
"description": "A readable Hacker News app.",
"icons": [
{
"src": "images/pwa-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/pwa-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=cheeaun.hackerweb"
}
]
}

25
cms/svelte.config.js Normal file
View file

@ -0,0 +1,25 @@
//import adapter from '@sveltejs/adapter-auto';
import adapter from '@sveltejs/adapter-node';
import preprocess from 'svelte-preprocess';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: [
vitePreprocess(),
preprocess({
postcss: true
})
],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

21
cms/tailwind.config.cjs Normal file
View file

@ -0,0 +1,21 @@
const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [require("daisyui")],
daisyui: {
styled: true,
themes: ["cupcake","cmyk"],
base: true,
utils: true,
logs: true,
rtl: false,
prefix: "",
darkTheme: "dark",
},
};
module.exports = config;

11
cms/tests/test.ts Normal file
View file

@ -0,0 +1,11 @@
import { expect, test } from '@playwright/test';
test('Index page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'กองสรรหาบุคคล (Recruitment Division)' })).toBeVisible();
});
test('About page has expected h1', async ({ page }) => {
await page.goto('/about');
await expect(page.getByRole('heading', { name: 'กองสรรหาบุคคล' })).toBeVisible();
});

17
cms/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

9
cms/vite.config.ts Normal file
View file

@ -0,0 +1,9 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});