├── env.d.ts
├── src
├── Application
│ ├── types.ts
│ ├── AppStartedUsecase.ts
│ ├── StopGameUsecase.ts
│ ├── StartGameUsecase.ts
│ └── OpenCardUsecase.ts
├── Presentation
│ ├── view
│ │ ├── assets
│ │ │ ├── card
│ │ │ │ ├── 01.jpg
│ │ │ │ ├── 02.jpg
│ │ │ │ ├── 03.jpg
│ │ │ │ ├── 04.jpg
│ │ │ │ ├── 05.jpg
│ │ │ │ ├── 06.jpg
│ │ │ │ ├── 07.jpg
│ │ │ │ └── 08.jpg
│ │ │ ├── github-mark.png
│ │ │ ├── bg-8-bit-nature.jpg
│ │ │ ├── logo.svg
│ │ │ ├── main.css
│ │ │ └── base.css
│ │ ├── utils
│ │ │ ├── get-image-url.ts
│ │ │ └── theme.ts
│ │ ├── router
│ │ │ └── index.ts
│ │ ├── scss
│ │ │ └── main.scss
│ │ ├── components
│ │ │ ├── AppModal.vue
│ │ │ ├── GameCard.vue
│ │ │ └── AppHeader.vue
│ │ └── pages
│ │ │ └── PageHome.vue
│ ├── presenter
│ │ ├── GamePresenter.ts
│ │ ├── ShowedCardsPresenter.ts
│ │ ├── PairCardAttempListPresenter.ts
│ │ └── CardPresenter.ts
│ └── usecaseMap
│ │ └── index.ts
├── Domain
│ ├── Game.ts
│ └── Card.ts
├── App.vue
├── Data
│ ├── game.store.ts
│ └── card.store.ts
├── Repositories
│ ├── GameRepository.ts
│ └── CardRepository.ts
└── main.ts
├── public
└── favicon.ico
├── .vscode
└── extensions.json
├── tsconfig.vitest.json
├── tsconfig.config.json
├── tsconfig.json
├── tsconfig.app.json
├── vite.config.ts
├── .gitignore
├── .eslintrc.cjs
├── index.html
├── package.json
└── README.md
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/Application/types.ts:
--------------------------------------------------------------------------------
1 | export interface Usecase {
2 | execute: () => unknown
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/01.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/02.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/03.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/04.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/05.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/06.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/06.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/07.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/07.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/card/08.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/card/08.jpg
--------------------------------------------------------------------------------
/src/Presentation/view/assets/github-mark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/github-mark.png
--------------------------------------------------------------------------------
/src/Presentation/view/assets/bg-8-bit-nature.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/attikos/ddd-vue-match-game/HEAD/src/Presentation/view/assets/bg-8-bit-nature.jpg
--------------------------------------------------------------------------------
/src/Domain/Game.ts:
--------------------------------------------------------------------------------
1 | export enum GameStatus {
2 | inProgress,
3 | stopped,
4 | }
5 |
6 | export interface GameState {
7 | gameStatus: GameStatus
8 | }
9 |
--------------------------------------------------------------------------------
/src/Presentation/view/utils/get-image-url.ts:
--------------------------------------------------------------------------------
1 | export function getImageUrl(name: string) {
2 | return new URL(`../assets/${name}`, import.meta.url).href
3 | }
4 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
--------------------------------------------------------------------------------
/tsconfig.vitest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "exclude": [],
4 | "compilerOptions": {
5 | "composite": true,
6 | "lib": [],
7 | "types": ["node", "jsdom"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.node.json",
3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.config.json"
6 | },
7 | {
8 | "path": "./tsconfig.app.json"
9 | },
10 | {
11 | "path": "./tsconfig.vitest.json"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.web.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "exclude": ["src/**/__tests__/*"],
5 | "compilerOptions": {
6 | "composite": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "@/*": ["./src/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Presentation/view/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Presentation/presenter/GamePresenter.ts:
--------------------------------------------------------------------------------
1 | import { useGameStore } from '@/Data/game.store';
2 | import { storeToRefs } from 'pinia';
3 | import type { Ref } from 'vue';
4 | import type { GameStatus } from '@/Domain/Game';
5 |
6 | export function GamePresenter(): Ref {
7 | const { gameStatus } = storeToRefs(useGameStore());
8 |
9 | return gameStatus;
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url';
2 |
3 | import { defineConfig } from 'vite';
4 | import vue from '@vitejs/plugin-vue';
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [vue()],
9 | resolve: {
10 | alias: {
11 | '@': fileURLToPath(new URL('./src', import.meta.url)),
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/src/Presentation/presenter/ShowedCardsPresenter.ts:
--------------------------------------------------------------------------------
1 | import { useCardStore } from '@/Data/card.store';
2 | import { storeToRefs } from 'pinia';
3 | import type { Ref } from 'vue';
4 | import type { ShowedCards } from '@/Domain/Card';
5 |
6 | export function ShowedCardsPresenter(): Ref {
7 | const { showedCards } = storeToRefs(useCardStore());
8 |
9 | return showedCards;
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
--------------------------------------------------------------------------------
/src/Presentation/view/router/index.ts:
--------------------------------------------------------------------------------
1 | import PageHome from '@/Presentation/view/pages/PageHome.vue';
2 | import { createRouter, createWebHashHistory } from 'vue-router'
3 |
4 | export const createAppRouter = () => {
5 | const routes = [{ path: '/', component: PageHome }];
6 |
7 | const router = createRouter({
8 | history: createWebHashHistory(),
9 | routes,
10 | });
11 |
12 | return router;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Presentation/presenter/PairCardAttempListPresenter.ts:
--------------------------------------------------------------------------------
1 | import { useCardStore } from '@/Data/card.store';
2 | import { storeToRefs } from 'pinia';
3 | import type { Ref } from 'vue';
4 | import type { PairCardAttempList } from '@/Domain/Card';
5 |
6 | export function PairCardAttempListPresenter(): Ref {
7 | const { pairCardAttempList } = storeToRefs(useCardStore());
8 |
9 | return pairCardAttempList;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Application/AppStartedUsecase.ts:
--------------------------------------------------------------------------------
1 | import type { Usecase } from '@/Application/types';
2 | import type { CardRepository } from '@/Repositories/CardRepository';
3 | import { cardImageAsset } from '@/Domain/Card';
4 |
5 | export class AppStartedUsecase implements Usecase {
6 | constructor(
7 | private cardRepository: CardRepository,
8 | ) {}
9 |
10 | async execute(): Promise {
11 | this.cardRepository.setCardImageAsset(cardImageAsset);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Presentation/view/utils/theme.ts:
--------------------------------------------------------------------------------
1 | export type Theme = 'cyan' | 'green' | 'blue' | 'purple' | 'red' | 'yellow';
2 |
3 | export const setTheme = (theme: Theme) => {
4 | const htmpEl = document.getElementsByTagName('html')[0];
5 |
6 | htmpEl.className = '';
7 | htmpEl.classList.add(theme);
8 | window.localStorage.setItem('theme', theme);
9 | }
10 |
11 | export const getSavedTheme = (): Theme => {
12 | const theme = window.localStorage.getItem('theme') as Theme;
13 |
14 | return theme;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Presentation/presenter/CardPresenter.ts:
--------------------------------------------------------------------------------
1 | import { useCardStore } from '@/Data/card.store';
2 | import { storeToRefs } from 'pinia';
3 | import type { Ref } from 'vue';
4 |
5 | export interface CardPresenter {
6 | cardImageAsset: Ref
7 | currentCards: Ref
8 | }
9 |
10 | export function CardPresenter(): CardPresenter {
11 | const { cardImageAsset, currentCards } = storeToRefs(useCardStore());
12 |
13 | return {
14 | cardImageAsset,
15 | currentCards,
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/src/Data/game.store.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import type { GameState } from '@/Domain/Game';
3 | import { GameStatus } from '@/Domain/Game';
4 |
5 | export const useGameStore = defineStore('game', {
6 | state: (): GameState => ({
7 | gameStatus : GameStatus.inProgress,
8 | }),
9 |
10 | actions: {
11 | setGameStatus(gameStatus: GameStatus) {
12 | this.gameStatus = gameStatus;
13 | },
14 | resetGameStatus() {
15 | this.gameStatus = GameStatus.stopped;
16 | },
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/Repositories/GameRepository.ts:
--------------------------------------------------------------------------------
1 | import { useGameStore } from '@/Data/game.store';
2 | import type { GameStatus } from '@/Domain/Game';
3 |
4 | export class GameRepository {
5 | private _store: ReturnType;
6 |
7 | constructor() {
8 | this._store = useGameStore();
9 | }
10 |
11 | get store() {
12 | return this._store;
13 | }
14 |
15 | setGameStatus(gameStatus: GameStatus) {
16 | this.store.setGameStatus(gameStatus);
17 | }
18 |
19 | resetGameStatus() {
20 | this.store.resetGameStatus();
21 | }
22 |
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution');
3 |
4 | module.exports = {
5 | root: true,
6 | extends: [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/eslint-config-typescript'
10 | ],
11 | parserOptions: {
12 | ecmaVersion: 'latest',
13 | },
14 | rules: {
15 | quotes : ['error', 'single'],
16 | 'vue/html-indent': ['error', 4],
17 | 'vue/multi-word-component-names': 0,
18 | '@typescript-eslint/indent': ['error', 4],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import { createPinia } from 'pinia';
3 | import { createAppRouter } from '@/Presentation/view/router';
4 | import App from '@/App.vue';
5 | import '@/Presentation/view/scss/main.scss';
6 | import { mapUsecase } from '@/Presentation/usecaseMap';
7 |
8 | const app = createApp(App);
9 |
10 | app.use(createPinia());
11 |
12 | const router = createAppRouter();
13 | app.use(router);
14 |
15 | router.isReady().then(async () => {
16 | const appStartedUsecase = mapUsecase('AppStartedUsecase');
17 |
18 | await appStartedUsecase();
19 |
20 | app.mount('#app');
21 | });
22 |
--------------------------------------------------------------------------------
/src/Presentation/view/assets/main.css:
--------------------------------------------------------------------------------
1 | @import './base.css';
2 |
3 | #app {
4 | max-width: 1280px;
5 | margin: 0 auto;
6 | padding: 2rem;
7 |
8 | font-weight: normal;
9 | }
10 |
11 | a,
12 | .green {
13 | text-decoration: none;
14 | color: hsla(160, 100%, 37%, 1);
15 | transition: 0.4s;
16 | }
17 |
18 | @media (hover: hover) {
19 | a:hover {
20 | background-color: hsla(160, 100%, 37%, 0.2);
21 | }
22 | }
23 |
24 | @media (min-width: 1024px) {
25 | body {
26 | display: flex;
27 | place-items: center;
28 | }
29 |
30 | #app {
31 | display: grid;
32 | grid-template-columns: 1fr 1fr;
33 | padding: 0 2rem;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Application/StopGameUsecase.ts:
--------------------------------------------------------------------------------
1 | import type { Usecase } from '@/Application/types';
2 | import { GameStatus } from '@/Domain/Game';
3 | import type { CardRepository } from '@/Repositories/CardRepository';
4 | import type { GameRepository } from '@/Repositories/GameRepository';
5 |
6 | export class StopGameUsecase implements Usecase {
7 | constructor(
8 | private cardRepository: CardRepository,
9 | private gameRepository: GameRepository
10 | ) {}
11 |
12 | async execute(): Promise {
13 | const STOP_GAME_ANIMATION_DELAY = 400;
14 |
15 | if (this.gameRepository.store.gameStatus === GameStatus.stopped) {
16 | return;
17 | }
18 |
19 | this.cardRepository.resetPairCardAttempList();
20 | this.cardRepository.openAllShowedCards();
21 |
22 | setTimeout(() => {
23 | this.gameRepository.setGameStatus(GameStatus.stopped);
24 | }, STOP_GAME_ANIMATION_DELAY)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Match Match Game
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Application/StartGameUsecase.ts:
--------------------------------------------------------------------------------
1 | import type { Usecase } from '@/Application/types';
2 | import { getRandomGameCards } from '@/Domain/Card';
3 | import { GameStatus } from '@/Domain/Game';
4 | import type { CardRepository } from '@/Repositories/CardRepository';
5 | import type { GameRepository } from '@/Repositories/GameRepository';
6 |
7 | export class StartGameUsecase implements Usecase {
8 | constructor(
9 | private cardRepository: CardRepository,
10 | private gameRepository: GameRepository
11 | ) {}
12 |
13 | async execute(): Promise {
14 | const START_GAME_ANIMATION_DELAY = 400;
15 | const cardImageAsset = this.cardRepository.store.cardImageAsset;
16 |
17 | this.cardRepository.resetShowedCards();
18 | this.cardRepository.resetPairCardAttempList();
19 |
20 | setTimeout(() => {
21 | const randomCards = getRandomGameCards(cardImageAsset);
22 | this.cardRepository.setCurrentCards(randomCards);
23 | this.gameRepository.setGameStatus(GameStatus.inProgress);
24 | }, START_GAME_ANIMATION_DELAY)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Repositories/CardRepository.ts:
--------------------------------------------------------------------------------
1 | import { useCardStore } from '@/Data/card.store';
2 | import type { PairCardAttempList, ShowedCards } from '@/Domain/Card';
3 |
4 | export class CardRepository {
5 | private _store: ReturnType;
6 |
7 | constructor() {
8 | this._store = useCardStore();
9 | }
10 |
11 | get store() {
12 | return this._store;
13 | }
14 |
15 | setCurrentCards(cards: string[]) {
16 | this.store.setCurrentCards(cards);
17 | }
18 |
19 | setCardImageAsset(cards: string[]) {
20 | this.store.setCardImageAsset(cards);
21 | }
22 |
23 | setShowedCards(cards: ShowedCards) {
24 | this.store.setShowedCards(cards);
25 | }
26 |
27 | addShowedCards(cards: ShowedCards) {
28 | this.store.addShowedCards(cards);
29 | }
30 |
31 | openAllShowedCards() {
32 | this.store.openAllShowedCards();
33 | }
34 |
35 | resetShowedCards() {
36 | this.store.resetShowedCards();
37 | }
38 |
39 | setPairCardAttempList(attemps: PairCardAttempList) {
40 | this.store.setPairCardAttempList(attemps);
41 | }
42 |
43 | resetPairCardAttempList() {
44 | this.store.resetPairCardAttempList();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3-demo",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "run-p type-check build-only",
8 | "preview": "vite preview",
9 | "test:unit": "vitest --environment jsdom --root src/",
10 | "build-only": "vite build",
11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
13 | },
14 | "dependencies": {
15 | "pinia": "^2.0.28",
16 | "vue": "^3.2.45"
17 | },
18 | "devDependencies": {
19 | "@rushstack/eslint-patch": "^1.1.4",
20 | "@types/jsdom": "^20.0.1",
21 | "@types/node": "^18.11.12",
22 | "@vitejs/plugin-vue": "^4.0.0",
23 | "@vue/eslint-config-typescript": "^11.0.0",
24 | "@vue/test-utils": "^2.2.6",
25 | "@vue/tsconfig": "^0.1.3",
26 | "eslint": "^8.22.0",
27 | "eslint-plugin-vue": "^9.3.0",
28 | "jsdom": "^20.0.3",
29 | "npm-run-all": "^4.1.5",
30 | "sass": "^1.57.1",
31 | "typescript": "~4.7.4",
32 | "vite": "^4.0.0",
33 | "vitest": "^0.25.6",
34 | "vue-router": "^4.1.6",
35 | "vue-tsc": "^1.0.12"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clean code. Domain Driven Design with Vue 3
2 | Used: Typescript, Vue 3, Pinia
3 |
4 | ### DEMO
5 | https://vuegit.ru
6 |
7 | ### Layers
8 | Flow of controls:
9 |
10 | ```
11 | USER
12 | ↓
13 | UI (Vue) ← Presentor
14 | ↓ ↑
15 | (entities) (usecases) (State, Pinia)
16 | Domain → Application Data
17 | ↓ ↓ ↑
18 | Adapters (Driven) → Repository
19 | ↓
20 | Infrastructure ← API, etc.
21 | ```
22 |
23 | Dependencies:
24 | ```
25 | USER
26 | ↓
27 | UI (Vue) ← Presentor
28 | ↓ ↑
29 | (entities) (usecases) (State, Pinia)
30 | Domain ← Application Data
31 | ↑ ↓
32 | Adapters (Driven) ← Repository
33 | ↑
34 | Infrastructure ← API, etc.
35 | ```
36 |
37 | ## Project Setup
38 |
39 | ```sh
40 | npm install
41 | ```
42 |
43 | Dev mode:
44 | ```sh
45 | npm run dev
46 | ```
47 | Build:
48 | ```sh
49 | npm run build
50 | ```
51 |
52 | # Links
53 | https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
54 |
--------------------------------------------------------------------------------
/src/Data/card.store.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import type { CardState, PairCardAttempList, ShowedCards } from '@/Domain/Card';
3 |
4 | export const useCardStore = defineStore('card', {
5 | state: (): CardState => ({
6 | cardImageAsset : [],
7 | currentCards : [],
8 | showedCards: {},
9 | pairCardAttempList: [],
10 | }),
11 |
12 | actions: {
13 | setCurrentCards(urls: string[]) {
14 | this.currentCards = urls;
15 | },
16 | setCardImageAsset(urls: string[]) {
17 | this.cardImageAsset = urls;
18 | },
19 | setShowedCards(cards: ShowedCards) {
20 | this.showedCards = cards;
21 | },
22 | addShowedCards(cards: ShowedCards) {
23 | this.$patch({ showedCards : cards });
24 | },
25 | openAllShowedCards() {
26 | const result: ShowedCards = {};
27 |
28 | this.currentCards.forEach((_, index) => {
29 | result[index] = true;
30 | });
31 |
32 | this.showedCards = result;
33 | },
34 | resetShowedCards() {
35 | this.showedCards = {};
36 | },
37 | setPairCardAttempList(attemps: PairCardAttempList) {
38 | this.pairCardAttempList = attemps;
39 | },
40 | resetPairCardAttempList() {
41 | this.pairCardAttempList = [];
42 | },
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/src/Presentation/usecaseMap/index.ts:
--------------------------------------------------------------------------------
1 | import { CardRepository } from '@/Repositories/CardRepository';
2 | import { AppStartedUsecase } from '@/Application/AppStartedUsecase';
3 | import { StartGameUsecase } from '@/Application/StartGameUsecase';
4 | import { OpenCardUsecase } from '@/Application/OpenCardUsecase';
5 | import { GameRepository } from '@/Repositories/GameRepository';
6 | import { StopGameUsecase } from '@/Application/StopGameUsecase';
7 |
8 | const usecaseMapping = {
9 | AppStartedUsecase: (): Promise => {
10 | const usecase = new AppStartedUsecase(
11 | new CardRepository(),
12 | );
13 |
14 | return usecase.execute();
15 | },
16 |
17 | StartGameUsecase: (): Promise => {
18 | const usecase = new StartGameUsecase(
19 | new CardRepository(),
20 | new GameRepository(),
21 | );
22 |
23 | return usecase.execute();
24 | },
25 |
26 | StopGameUsecase: (): Promise => {
27 | const usecase = new StopGameUsecase(
28 | new CardRepository(),
29 | new GameRepository(),
30 | );
31 |
32 | return usecase.execute();
33 | },
34 |
35 | OpenCardUsecase: (index: number, showWonNotification: () => void): Promise => {
36 | const usecase = new OpenCardUsecase(
37 | index,
38 | new CardRepository(),
39 | new GameRepository(),
40 | showWonNotification,
41 | );
42 |
43 | return usecase.execute();
44 | }
45 | }
46 |
47 | export const mapUsecase = (usecase: T) => {
48 | return usecaseMapping[usecase];
49 | };
50 |
--------------------------------------------------------------------------------
/src/Presentation/view/scss/main.scss:
--------------------------------------------------------------------------------
1 | @mixin theme-color($name, $hue) {
2 | --hue-#{"" + $name}: #{$hue};
3 | --color-#{"" + $name}: #{'hsl(var(--hue-#{$name}), 71%, 46%)'};
4 |
5 | &.#{"" + $name} {
6 | --primary-hue: var(--hue-#{$name});
7 | }
8 | }
9 |
10 | html {
11 | &:root {
12 | --text-color: #444;
13 | --border-color: #999;
14 | --main-bg-color: #b1ddf8;
15 | --color-shaddow-first: #ff98fc;
16 | --color-shaddow-second: #5af7ff;
17 | --primary-hue: 58deg;
18 | --primary-color: hsl(var(--primary-hue), 72%, 44%);
19 | --secondary-color: hsl(var(--primary-hue), 70%, 60%);
20 | --button-text-color: #fff;
21 | --button-bg-color: var(--primary-color);
22 | --button-text-color-hover: #ddd;
23 | --button-text-color-active: #ccc;
24 |
25 | @include theme-color(cyan, 180deg);
26 | @include theme-color(green, 115deg);
27 | @include theme-color(blue, 200deg);
28 | @include theme-color(purple, 280deg);
29 | @include theme-color(red, 5deg);
30 | @include theme-color(yellow, 58deg);
31 | }
32 | }
33 |
34 | body {
35 | font-family: MS Sans Serif, Geneva, sans-serif;
36 | padding: 0;
37 | margin: 0;
38 | background: url(../assets/bg-8-bit-nature.jpg) no-repeat center;
39 | background-color: var(--main-bg-color);
40 | background-size: cover;
41 | }
42 |
43 | .btn {
44 | color: var(--button-text-color);
45 | background: var(--primary-color);
46 | padding: 6px 12px;
47 | font-size: 16px;
48 | cursor: pointer;
49 | border: 1px solid var(--border-color);
50 | display: inline-block;
51 | min-width: 96px;
52 |
53 | &:hover {
54 | color: var(--button-text-color-hover);
55 | }
56 |
57 | &:active {
58 | color: var(--button-text-color-active);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Presentation/view/assets/base.css:
--------------------------------------------------------------------------------
1 | /* color palette from */
2 | :root {
3 | --vt-c-white: #ffffff;
4 | --vt-c-white-soft: #f8f8f8;
5 | --vt-c-white-mute: #f2f2f2;
6 |
7 | --vt-c-black: #181818;
8 | --vt-c-black-soft: #222222;
9 | --vt-c-black-mute: #282828;
10 |
11 | --vt-c-indigo: #2c3e50;
12 |
13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17 |
18 | --vt-c-text-light-1: var(--vt-c-indigo);
19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20 | --vt-c-text-dark-1: var(--vt-c-white);
21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22 | }
23 |
24 | /* semantic color variables for this project */
25 | :root {
26 | --color-background: var(--vt-c-white);
27 | --color-background-soft: var(--vt-c-white-soft);
28 | --color-background-mute: var(--vt-c-white-mute);
29 |
30 | --color-border: var(--vt-c-divider-light-2);
31 | --color-border-hover: var(--vt-c-divider-light-1);
32 |
33 | --color-heading: var(--vt-c-text-light-1);
34 | --color-text: var(--vt-c-text-light-1);
35 |
36 | --section-gap: 160px;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | :root {
41 | --color-background: var(--vt-c-black);
42 | --color-background-soft: var(--vt-c-black-soft);
43 | --color-background-mute: var(--vt-c-black-mute);
44 |
45 | --color-border: var(--vt-c-divider-dark-2);
46 | --color-border-hover: var(--vt-c-divider-dark-1);
47 |
48 | --color-heading: var(--vt-c-text-dark-1);
49 | --color-text: var(--vt-c-text-dark-2);
50 | }
51 | }
52 |
53 | *,
54 | *::before,
55 | *::after {
56 | box-sizing: border-box;
57 | margin: 0;
58 | position: relative;
59 | font-weight: normal;
60 | }
61 |
62 | body {
63 | min-height: 100vh;
64 | color: var(--color-text);
65 | background: var(--color-background);
66 | transition: color 0.5s, background-color 0.5s;
67 | line-height: 1.6;
68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
70 | font-size: 15px;
71 | text-rendering: optimizeLegibility;
72 | -webkit-font-smoothing: antialiased;
73 | -moz-osx-font-smoothing: grayscale;
74 | }
75 |
--------------------------------------------------------------------------------
/src/Domain/Card.ts:
--------------------------------------------------------------------------------
1 | export const cardImageAsset = [
2 | 'card/01.jpg',
3 | 'card/02.jpg',
4 | 'card/03.jpg',
5 | 'card/04.jpg',
6 | 'card/05.jpg',
7 | 'card/06.jpg',
8 | 'card/07.jpg',
9 | 'card/08.jpg',
10 | ];
11 |
12 | export interface PairCardAttemp {
13 | index: number
14 | id: string
15 | }
16 |
17 | export type PairCardAttempList = [PairCardAttemp?, PairCardAttemp?];
18 |
19 | export interface ShowedCards {
20 | [key: number]: boolean
21 | }
22 |
23 | export type CurrentCard = string;
24 | export type CardImage = string;
25 | export type CardImageAsset = CardImage[];
26 |
27 | export interface CardState {
28 | cardImageAsset: CardImageAsset
29 | currentCards: CurrentCard[]
30 | showedCards: ShowedCards
31 | pairCardAttempList: PairCardAttempList
32 | }
33 |
34 | export function checkPairCards(pairCardAttemp: PairCardAttempList): boolean {
35 | return pairCardAttemp[0]?.id === pairCardAttemp[1]?.id;
36 | }
37 |
38 | export function removeAttempsCards(
39 | showedCardList: ShowedCards,
40 | pairCards: PairCardAttempList
41 | ): ShowedCards {
42 | const result = { ...showedCardList };
43 |
44 | pairCards.forEach(card => {
45 | if (card && typeof card.index === 'number') {
46 | delete result[card.index];
47 | }
48 | });
49 |
50 | return result;
51 | }
52 |
53 | export function checkIsShowedCard (showedCards: ShowedCards, index: number): boolean {
54 | return !!showedCards[index];
55 | }
56 |
57 | export function addCardAttemps(
58 | pairCardAttempList: PairCardAttempList,
59 | currentCards: CurrentCard[],
60 | index: number
61 | ): PairCardAttempList {
62 | const result : PairCardAttempList = [];
63 | pairCardAttempList.forEach(val => result.push(Object.assign({}, val)));
64 |
65 | if (result.length >= 2) {
66 | return result;
67 | }
68 |
69 | result.push({
70 | index,
71 | id: currentCards[index]
72 | });
73 |
74 | return result;
75 | }
76 |
77 | export function checkIsFinish(showedCardList: ShowedCards, currentCards: CurrentCard[]): boolean {
78 | return Object.keys(showedCardList).length === currentCards.length;
79 | }
80 |
81 | export function getRandomGameCards(cardImageAsset: CardImageAsset) {
82 | return [...cardImageAsset, ...cardImageAsset].sort(() => (Math.random() > 0.5) ? 1 : -1);
83 | }
84 |
--------------------------------------------------------------------------------
/src/Presentation/view/components/AppModal.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
29 |
30 |
31 |
32 |
33 |
101 |
--------------------------------------------------------------------------------
/src/Application/OpenCardUsecase.ts:
--------------------------------------------------------------------------------
1 | import type { Usecase } from '@/Application/types';
2 | import { addCardAttemps, checkPairCards, checkIsFinish, removeAttempsCards, checkIsShowedCard } from '@/Domain/Card';
3 | import { GameStatus } from '@/Domain/Game';
4 | import type { CardRepository } from '@/Repositories/CardRepository';
5 | import type { GameRepository } from '@/Repositories/GameRepository';
6 |
7 | export class OpenCardUsecase implements Usecase {
8 | constructor(
9 | private index: number,
10 | private cardRepository: CardRepository,
11 | private gameRepository: GameRepository,
12 | private showWonNotification: () => void,
13 | ) {}
14 |
15 | async execute(): Promise {
16 | const DELAY_TO_CLOSE = 1000;
17 | const resetPairAttemps = () => this.cardRepository.setPairCardAttempList([]);
18 | const { showedCards } = this.cardRepository.store;
19 |
20 | if (checkIsShowedCard(showedCards, this.index)) {
21 | return;
22 | }
23 |
24 | let { pairCardAttempList } = this.cardRepository.store;
25 |
26 | if (pairCardAttempList.length >= 2) {
27 | return;
28 | }
29 |
30 | const { currentCards } = this.cardRepository.store;
31 |
32 | const cardAttemps = addCardAttemps(
33 | pairCardAttempList,
34 | currentCards,
35 | this.index
36 | );
37 |
38 | this.cardRepository.setPairCardAttempList(cardAttemps);
39 | pairCardAttempList = this.cardRepository.store.pairCardAttempList;
40 |
41 | this.cardRepository.addShowedCards({ [this.index] : true });
42 |
43 | if (pairCardAttempList.length === 2) {
44 | let newShowedCards = this.cardRepository.store.showedCards;
45 |
46 | if (!checkPairCards(pairCardAttempList)) {
47 | newShowedCards = removeAttempsCards(
48 | showedCards,
49 | pairCardAttempList
50 | );
51 | }
52 | else {
53 | const { currentCards } = this.cardRepository.store;
54 |
55 | const isFinishedGame = checkIsFinish(
56 | showedCards,
57 | currentCards
58 | );
59 |
60 | if (isFinishedGame) {
61 | this.cardRepository.setShowedCards(newShowedCards);
62 | this.showWonNotification();
63 | this.gameRepository.setGameStatus(GameStatus.stopped);
64 | }
65 | }
66 |
67 | setTimeout(() => {
68 | const { gameStatus } = this.gameRepository.store;
69 |
70 | if (gameStatus === GameStatus.stopped) {
71 | return;
72 | }
73 |
74 | this.cardRepository.setShowedCards(newShowedCards);
75 |
76 | resetPairAttemps();
77 | }, DELAY_TO_CLOSE);
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Presentation/view/components/GameCard.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
37 |
38 |
39 |
![]()
45 |
46 |
47 |
48 |
49 |
123 |
--------------------------------------------------------------------------------
/src/Presentation/view/pages/PageHome.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
109 |
--------------------------------------------------------------------------------
/src/Presentation/view/components/AppHeader.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
64 |
65 |
66 |
195 |
--------------------------------------------------------------------------------