First Commit

This commit is contained in:
tmddn3070 2024-07-17 10:56:48 +09:00
commit d3d51152cf
32 changed files with 5136 additions and 0 deletions

15
.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:lts-alpine
WORKDIR /app
COPY . ./
RUN npm install
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

4493
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "client",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"axios": "^1.6.7",
"pinia": "^2.1.7",
"react-dnd-html5-backend": "^16.0.1",
"tailwind-merge": "^2.2.1",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"vue3-dnd": "^2.1.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.10",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.18",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"npm-run-all2": "^6.1.1",
"postcss": "^8.4.35",
"prettier": "^3.0.3",
"tailwindcss": "^3.4.1",
"typescript": "~5.3.0",
"vite": "^5.0.11",
"vue-tsc": "^1.8.27"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

16
src/App.vue Normal file
View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import {RouterLink, RouterView} from 'vue-router'
</script>
<template>
<div class="bg-gray-50 min-h-screen pt-24 px-4">
<div class="">
<RouterView/>
</div>
</div>
</template>
<style scoped>
</style>

3
src/assets/main.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import Resource from "@/components/Resource.vue";
import {useResourcesStore} from "@/stores/useResourcesStore";
import {storeToRefs} from "pinia";
import {computed, ref} from "vue";
const store = useResourcesStore()
const {resources} = storeToRefs(store)
const searchTerm = ref('')
const filteredResources = computed(() => {
return resources.value.filter((resource) => {
return resource.title.toLowerCase().includes(searchTerm.value.toLowerCase())
})
})
</script>
<template>
<div class="flex gap-3 flex-wrap pt-3">
<input v-model="searchTerm" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2" placeholder="단어 검색하기">
<Resource v-for="resource in filteredResources" :key="resource.title" :title="resource.title" :emoji="resource.emoji"></Resource>
</div>
</template>
<style scoped>
</style>

52
src/components/Box.vue Normal file
View File

@ -0,0 +1,52 @@
<script lang="ts" setup>
import {useDrag} from 'vue3-dnd'
import {ItemTypes} from './ItemTypes'
import {toRefs} from '@vueuse/core'
const props = defineProps<{
id: any
left: number
top: number
hideSourceOnDrag?: boolean
loading?: boolean
}>()
const [collect, drag] = useDrag(() => ({
type: ItemTypes.BOX,
item: {id: props.id, left: props.left, top: props.top},
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
}))
const {isDragging} = toRefs(collect)
</script>
<template>
<div v-if="isDragging && hideSourceOnDrag" :ref="drag"/>
<div
v-else
:ref="drag"
class="absolute"
:style="{ left: `${left}px`, top: `${top}px` }"
role="Box"
data-testid="box"
>
<div v-if="loading">
<div
class="border-gray-200 shadow hover:bg-gray-100 cursor-pointer transition inline-flex items-center text-2xl space-x-2.5 py-2.5 px-4 font-medium border rounded-lg ">
<svg class="animate-spin -ml-1 mr-2 h-6 w-6 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>
생성중
</span>
</div>
</div>
<slot v-else></slot>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,87 @@
<script lang="ts" setup>
import {useDrop, XYCoord} from 'vue3-dnd'
import {ItemTypes} from './ItemTypes'
import Box from './Box.vue'
import type {DragItem} from './interfaces'
import {reactive, ref} from 'vue'
import ItemCard from "@/components/ItemCard.vue";
import AvailableResources from "@/components/AvailableResources.vue";
import {useBoxesStore} from "@/stores/useBoxesStore";
import {storeToRefs} from "pinia";
const store = useBoxesStore()
const { boxes } = store
const moveBox = (id: string, left: number, top: number, title?: string, emoji?: string) => {
if (id) {
Object.assign(boxes[id], {left, top})
} else {
const key = Math.random().toString(36).substring(7);
boxes[key] = {top, left, title, emoji}
console.log(boxes)
}
}
const containerElement = ref<HTMLElement | null>(null)
const [, drop] = useDrop(() => ({
accept: ItemTypes.BOX,
drop(item: DragItem, monitor) {
if (item.id && item.left !== null && item.top !== null) {
const delta = monitor.getDifferenceFromInitialOffset() as XYCoord
if(delta && delta.x && delta.y){
const left = Math.round((item.left) + delta.x)
const top = Math.round((item.top) + delta.y )
moveBox(item.id, left, top)
}
} else {
const delta = monitor.getClientOffset() as XYCoord
// current mouse position relative to drop
const containerCoords = containerElement.value.getBoundingClientRect()
if(delta && delta.x && delta.y){
const left = Math.round(delta.x - containerCoords.left - 40)
const top = Math.round(delta.y - containerCoords.top - 15)
moveBox(null, left, top, item.title, item.emoji)
}
}
return undefined
},
}))
</script>
<template>
<div ref="containerElement">
<main class="flex gap-x-3">
<div class="w-3/4">
<div :ref="drop" class="container">
<Box
v-for="(value, key) in boxes"
:id="key"
:key="key"
:left="value.left"
:top="value.top"
:loading="value.loading"
>
<ItemCard size="large" :id="key" :title="value.title" :emoji="value.emoji"/>
</Box>
</div>
</div>
<div class="w-1/4 bg-white shadow px-4 py-3 border-gray-200 border rounded-lg overflow-y-scroll max-h-[80vh]">
<h2 class="font-semibold">단어</h2>
<AvailableResources></AvailableResources>
</div>
</main>
</div>
</template>
<style scoped>
.container {
position: relative;
width: 100%;
height: 90vh;
}
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import Container from './Container.vue'
import { DndProvider } from 'vue3-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
</script>
<template>
<div>
<DndProvider :backend="HTML5Backend">
<Container />
</DndProvider>
</div>
</template>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import {useDrop} from "vue3-dnd";
import {ItemTypes} from "@/components/ItemTypes";
import {DragItem} from "@/components/interfaces";
import {useBoxesStore} from "@/stores/useBoxesStore";
import axios from "axios";
import {useResourcesStore} from "@/stores/useResourcesStore";
import {storeToRefs} from "pinia";
import {twMerge} from "tailwind-merge";
const props = defineProps<{
title: string;
emoji: string;
id: string;
size: 'small' | 'large';
}>()
const store = useBoxesStore()
const {removeBox, addBox} = store
const {resources} = storeToRefs(useResourcesStore())
const {addResource} =useResourcesStore()
const [, drop] = useDrop(() => ({
accept: ItemTypes.BOX,
async drop(item: DragItem, monitor) {
if (item.id !== props.id) {
const droppedId = item?.id;
const secondTitle = store.boxes[droppedId]?.title ?? item?.title
const secondEmoji = store.boxes[droppedId]?.emoji ?? item?.emoji
if(droppedId){
removeBox(droppedId);
}
const second = `${secondEmoji} ${secondTitle}`
store.boxes[props.id].loading = true
const response = await axios.post('https://craft.runa.pw/v1/merge', {
first_word: `${store.boxes[props.id].emoji} ${store.boxes[props.id].title}`,
second_word: second
})
const resultAnswer = response.data.response.word !== '' ? response.data.response.word : store.boxes[props.id].title
const resultEmoji = response.data.response.emoji !== '' ? response.data.response.emoji : store.boxes[props.id].emoji
addBox({
title: resultAnswer,
emoji: resultEmoji,
left: store.boxes[props.id].left,
top: store.boxes[props.id].top
})
if(!resources.value.find((resource: { title: string; }) => resource.title === resultAnswer)){
addResource({
title: resultAnswer,
emoji: resultEmoji
})
}
removeBox(props.id);
}
},
}))
</script>
<template>
<div :ref="drop"
:class="twMerge(props.size === 'large' ? 'text-2xl space-x-2.5 py-2.5 px-4' : 'space-x-1.5 px-3 py-1','border-gray-200 bg-white shadow hover:bg-gray-100 cursor-pointer transition inline-block font-medium border rounded-lg ')">
<span>
{{ emoji }}
</span>
<span>
{{ title }}
</span>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,3 @@
export const ItemTypes = {
BOX: 'box',
}

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import { useDrag } from 'vue3-dnd'
import { ItemTypes } from './ItemTypes'
import { toRefs } from '@vueuse/core'
import ItemCard from "@/components/ItemCard.vue";
const props = defineProps<{
emoji: string
title: string
}>()
const [collect, drag] = useDrag(() => ({
type: ItemTypes.BOX,
item: { title: props.title, emoji: props.emoji },
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
}))
const { isDragging } = toRefs(collect)
</script>
<template>
<div
class="inline-block"
:ref="drag"
role="Box"
data-testid="box"
>
<ItemCard :title="title" :emoji="emoji"></ItemCard>
</div>
</template>
<style scoped>
</style>

3
src/components/index.ts Normal file
View File

@ -0,0 +1,3 @@
import Example from './Example.vue'
export default Example

View File

@ -0,0 +1,8 @@
export interface DragItem {
type: string
id: string
top: number|null
left: number|null
emoji: string
title: string
}

14
src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

23
src/router/index.ts Normal file
View File

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})
export default router

View File

@ -0,0 +1,30 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import {reactive} from "vue";
export interface BoxStoreEntry {
top: number
left: number
title: string
emoji: string
loading?: boolean
}
export const useBoxesStore = defineStore('counter', () => {
const boxes = reactive<{
[key: string]: BoxStoreEntry
}>({
a: {top: 20, left: 80, title: '불', emoji: '🔥'},
})
function addBox(box: BoxStoreEntry) {
const randomId = Math.random().toString(36).substr(2, 5)
boxes[randomId] = box
}
function removeBox(id: string) {
delete boxes[id]
}
return { boxes , removeBox, addBox}
})

View File

@ -0,0 +1,28 @@
import {ref} from 'vue'
import {defineStore} from 'pinia'
import {useLocalStorage} from "@vueuse/core";
export interface ResourceStoreEntry {
title: string
emoji: string
}
export const useResourcesStore = defineStore('resources', () => {
const resources =
useLocalStorage<ResourceStoreEntry[]>('opencraft/resources', [
{title: '불', emoji: '🔥'},
{title: '물', emoji: '💧'},
{title: '지구', emoji: '🌍'},
{title: '공기', emoji: '💨'},
{title: '빛', emoji: '💡'},
{title: '어둠', emoji: '🌑'},
{title: '생명', emoji: '🌱'},
{title: '시간', emoji: '⏳'},
{title: '사람', emoji: '👤'},
]);
function addResource(box: ResourceStoreEntry) {
resources.value.push(box)
}
return { resources, addResource}
})

15
src/views/AboutView.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

11
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import Example from "@/components/Example.vue";
import ItemCard from "@/components/ItemCard.vue";
import Resource from "@/components/Resource.vue";
import AvaliableResources from "@/components/AvailableResources.vue";
import Container from "@/components/Container.vue";
</script>
<template>
<Example></Example>
</template>

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

14
tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

16
vite.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})