Compare commits
No commits in common. "afd2345cac21a492849d498676839db7a2b0b972" and "c5d6075c0145bda81cc582258f8993b494003d16" have entirely different histories.
afd2345cac
...
c5d6075c01
|
|
@ -5,7 +5,6 @@ plugins {
|
||||||
kotlin("jvm") version "1.9.24"
|
kotlin("jvm") version "1.9.24"
|
||||||
kotlin("plugin.spring") version "1.9.24"
|
kotlin("plugin.spring") version "1.9.24"
|
||||||
kotlin("plugin.allopen") version "1.9.22"
|
kotlin("plugin.allopen") version "1.9.22"
|
||||||
kotlin("kapt") version "1.9.10"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "at.eisibaer"
|
group = "at.eisibaer"
|
||||||
|
|
@ -27,8 +26,7 @@ repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
val bcVersion: String = "1.78.1"
|
val bcVersion: String = "1.78.1";
|
||||||
val mapstructVersion: String = "1.6.0"
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
|
|
@ -41,12 +39,10 @@ dependencies {
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
implementation("org.bouncycastle:bcprov-jdk18on:$bcVersion")
|
implementation("org.bouncycastle:bcprov-jdk18on:$bcVersion")
|
||||||
implementation("org.mapstruct:mapstruct:$mapstructVersion")
|
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
kapt("org.mapstruct:mapstruct-processor:$mapstructVersion")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
testImplementation("org.springframework.security:spring-security-test")
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
|
|
@ -59,12 +55,6 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
|
||||||
arguments {
|
|
||||||
arg("mapstruct.defaultComponentModel", "spring")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,4 @@ data class ApplicationProperties(
|
||||||
val test: Boolean,
|
val test: Boolean,
|
||||||
val corsAllowedOrigins: List<String>,
|
val corsAllowedOrigins: List<String>,
|
||||||
val corsAllowedMethods: List<String>,
|
val corsAllowedMethods: List<String>,
|
||||||
|
)
|
||||||
val storage: StorageProperties,
|
|
||||||
){
|
|
||||||
|
|
||||||
|
|
||||||
data class StorageProperties (
|
|
||||||
|
|
||||||
val fs: FileSystemStorageProperties,
|
|
||||||
) {
|
|
||||||
data class FileSystemStorageProperties(
|
|
||||||
|
|
||||||
val location: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,5 @@ class SpringConfiguration(
|
||||||
.addMapping("/api/**")
|
.addMapping("/api/**")
|
||||||
.allowedOrigins(*applicationProperties.corsAllowedOrigins.map { it }.toTypedArray())
|
.allowedOrigins(*applicationProperties.corsAllowedOrigins.map { it }.toTypedArray())
|
||||||
.allowedMethods(*applicationProperties.corsAllowedMethods.map { it }.toTypedArray())
|
.allowedMethods(*applicationProperties.corsAllowedMethods.map { it }.toTypedArray())
|
||||||
// registry
|
|
||||||
// .addMapping("/gs-guide-websocket")
|
|
||||||
// .allowedOrigins(*applicationProperties.corsAllowedOrigins.map { it }.toTypedArray())
|
|
||||||
// .allowedMethods(*applicationProperties.corsAllowedMethods.map { it }.toTypedArray())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,28 +2,20 @@ package at.eisibaer.jbear2.config
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
|
||||||
import org.springframework.session.Session
|
|
||||||
import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer
|
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
|
||||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableScheduling
|
|
||||||
@EnableWebSocketMessageBroker
|
@EnableWebSocketMessageBroker
|
||||||
class WebSocketConfig(
|
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
|
||||||
val applicationProperties: ApplicationProperties,
|
|
||||||
) : AbstractSessionWebSocketMessageBrokerConfigurer<Session>() {
|
|
||||||
|
|
||||||
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
|
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
|
||||||
|
registry.enableSimpleBroker("/game");
|
||||||
registry.setApplicationDestinationPrefixes("/app");
|
registry.setApplicationDestinationPrefixes("/app");
|
||||||
registry.enableSimpleBroker("/topic");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun configureStompEndpoints(registry: StompEndpointRegistry) {
|
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
|
||||||
registry
|
registry.addEndpoint("/websocket")
|
||||||
.addEndpoint("/ws")
|
|
||||||
.setAllowedOrigins(applicationProperties.corsAllowedOrigins.joinToString(","))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.board
|
|
||||||
|
|
||||||
import org.springframework.data.geo.Point
|
|
||||||
|
|
||||||
data class AnswerDto(
|
|
||||||
val text: String,
|
|
||||||
val answerType: Int,
|
|
||||||
val image: String?,
|
|
||||||
val location: Point?,
|
|
||||||
)
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.board
|
|
||||||
|
|
||||||
data class BoardDto(
|
|
||||||
val id: Long?,
|
|
||||||
val boardName: String,
|
|
||||||
val owner: String?,
|
|
||||||
val categories: List<CategoryDto>,
|
|
||||||
val pointsAreTitle: Boolean,
|
|
||||||
)
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.board
|
|
||||||
|
|
||||||
data class BoardEntryDto(
|
|
||||||
val name: String,
|
|
||||||
val points: Long,
|
|
||||||
val answer: AnswerDto,
|
|
||||||
val questions: List<QuestionDto>,
|
|
||||||
// val category: CategoryDto,
|
|
||||||
)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.board
|
|
||||||
|
|
||||||
data class CategoryDto(
|
|
||||||
val name: String,
|
|
||||||
val description: String,
|
|
||||||
val boardEntries: List<BoardEntryDto>,
|
|
||||||
)
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.board
|
|
||||||
|
|
||||||
import org.springframework.data.geo.Point
|
|
||||||
|
|
||||||
data class QuestionDto(
|
|
||||||
val text: String,
|
|
||||||
val questionType: Int,
|
|
||||||
val fontScaling: Int,
|
|
||||||
val image: String?,
|
|
||||||
val location: Point?,
|
|
||||||
// val boardEntryDto: BoardEntryDto,
|
|
||||||
)
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.game
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class GameDto(
|
|
||||||
val inviteCode: String,
|
|
||||||
val uuid: UUID,
|
|
||||||
val boardId: Long,
|
|
||||||
// val host: String,
|
|
||||||
val players: List<PlayerDto>,
|
|
||||||
// val alreadyAnsweredEntries: List<Long>,
|
|
||||||
val acceptingAnswers: Boolean = false,
|
|
||||||
val currentlyChoosingPlayer: PlayerDto? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.game
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
data class PlayerDto(
|
|
||||||
val name: String,
|
|
||||||
val id: Long,
|
|
||||||
// val uuid: UUID,
|
|
||||||
)
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.message
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class GameMessage(val uuid: UUID, val message: String);
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.message
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.BoardEntry
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
data class GameStateMessage(
|
|
||||||
val uuid: UUID,
|
|
||||||
val categoryIndex: Int?,
|
|
||||||
val boardEntryIndex: Int?,
|
|
||||||
val questionIndex: Int?,
|
|
||||||
val questionLayerIndex: Int?,
|
|
||||||
)
|
|
||||||
|
|
@ -4,5 +4,8 @@ import lombok.AllArgsConstructor
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
data class GenericMessage (
|
data class GenericMessage (
|
||||||
val content: String,
|
|
||||||
|
var name: String? = "",
|
||||||
|
|
||||||
|
var content: String? = "",
|
||||||
)
|
)
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.message
|
|
||||||
|
|
||||||
data class JoinGameMessage(
|
|
||||||
val playerName: String,
|
|
||||||
)
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.message
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.game.PlayerDto
|
|
||||||
|
|
||||||
data class PlayerMessage(
|
|
||||||
val player: PlayerDto,
|
|
||||||
val content: String,
|
|
||||||
)
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package at.eisibaer.jbear2.dto.user
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.BoardDto
|
|
||||||
|
|
||||||
data class UserDto(
|
|
||||||
val username: String,
|
|
||||||
val profilePicture: String,
|
|
||||||
val boards: List<BoardDto>,
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package at.eisibaer.jbear2.endpoint
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/api/board")
|
||||||
|
class BoardEndpoint {
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package at.eisibaer.jbear2.endpoint
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.exception.StorageFileNotFoundException
|
|
||||||
import at.eisibaer.jbear2.service.FileService
|
|
||||||
import at.eisibaer.jbear2.service.StorageService
|
|
||||||
import org.springframework.http.ResponseEntity
|
|
||||||
import org.springframework.stereotype.Controller
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
|
||||||
import org.springframework.web.multipart.MultipartFile
|
|
||||||
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class FileEndpoint(
|
|
||||||
val storageService: StorageService,
|
|
||||||
val fileService: FileService,
|
|
||||||
) {
|
|
||||||
|
|
||||||
@PostMapping("/")
|
|
||||||
fun handleFileUpload( @RequestParam("files") files: List<MultipartFile> ): ResponseEntity<*> {
|
|
||||||
fileService.saveFiles(files);
|
|
||||||
|
|
||||||
return ResponseEntity.ok().build<Any>()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(StorageFileNotFoundException::class)
|
|
||||||
fun handleStorageFileNotFound(exc: StorageFileNotFoundException?): ResponseEntity<*> {
|
|
||||||
return ResponseEntity.notFound().build<Any>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package at.eisibaer.jbear2.endpoint
|
||||||
|
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping
|
||||||
|
import org.springframework.messaging.handler.annotation.SendTo
|
||||||
|
|
||||||
|
import at.eisibaer.jbear2.dto.message.GenericMessage
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/game")
|
||||||
|
class GameEndpoint {
|
||||||
|
|
||||||
|
@MessageMapping("/player/join")
|
||||||
|
@SendTo("/player/joined")
|
||||||
|
fun playerJoining(playerJoiningMessage: GenericMessage): GenericMessage{
|
||||||
|
return playerJoiningMessage.copy(playerJoiningMessage.name, "Joined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,17 @@
|
||||||
package at.eisibaer.jbear2.endpoint
|
package at.eisibaer.jbear2.endpoint
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.BoardDto
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import at.eisibaer.jbear2.repository.UserRepository
|
|
||||||
import at.eisibaer.jbear2.security.UserDetailsImpl
|
|
||||||
import at.eisibaer.jbear2.service.BoardService
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/user")
|
@RequestMapping("/api/user")
|
||||||
class UserEndpoint(
|
class UserEndpoint {
|
||||||
val boardService: BoardService,
|
|
||||||
val userRepository: UserRepository,
|
|
||||||
val fileRepository: FileRepository,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val log: Logger = LoggerFactory.getLogger(UserEndpoint::class.java);
|
val log: Logger = LoggerFactory.getLogger(UserEndpoint::class.java);
|
||||||
|
|
||||||
|
|
@ -30,56 +22,7 @@ class UserEndpoint(
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/boards")
|
@GetMapping("/boards")
|
||||||
fun getBoards(): ResponseEntity<List<BoardDto>?>{
|
fun getBoards(){
|
||||||
log.info("Get all Board Endpoint");
|
TODO();
|
||||||
val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl;
|
|
||||||
val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB");
|
|
||||||
|
|
||||||
return boardService.getBoardsByUser( user );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/boards/{boardId}")
|
|
||||||
fun getBoardById(@PathVariable boardId: Long): ResponseEntity<BoardDto?>{
|
|
||||||
log.info("Get Board Endpoint with id {}", boardId);
|
|
||||||
val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl
|
|
||||||
val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB");
|
|
||||||
|
|
||||||
return boardService.getBoardByUserAndId( user, boardId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/boards")
|
|
||||||
fun saveNewBoard(@RequestBody boardDto: BoardDto): ResponseEntity<*> {
|
|
||||||
log.info("Post Board Endpoint");
|
|
||||||
val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl
|
|
||||||
val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB");
|
|
||||||
|
|
||||||
if( boardDto.id != null ){
|
|
||||||
return ResponseEntity.badRequest().body(Unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return boardService.saveBoardToUser( user, boardDto );
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/boards/{boardId}")
|
|
||||||
fun saveExistingBoard(@RequestBody boardDto: BoardDto, @PathVariable boardId: Long): ResponseEntity<*> {
|
|
||||||
log.info("Put Board Endpoint with id {}", boardId);
|
|
||||||
val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl
|
|
||||||
val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB");
|
|
||||||
|
|
||||||
if( boardDto.id == null || boardDto.id != boardId ){
|
|
||||||
return ResponseEntity.badRequest().body("ID is either null or does not match")
|
|
||||||
}
|
|
||||||
|
|
||||||
return boardService.saveBoardToUser( user, boardDto );
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/boards/{boardId}")
|
|
||||||
fun saveNewBoard(@PathVariable boardId: Long): ResponseEntity<*> {
|
|
||||||
log.info("Delete Board Endpoint with id {}", boardId);
|
|
||||||
val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl
|
|
||||||
val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB");
|
|
||||||
|
|
||||||
return boardService.deleteBoardOfUser( user, boardId );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
package at.eisibaer.jbear2.endpoint.ws
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.QuestionDto
|
|
||||||
import at.eisibaer.jbear2.dto.message.*
|
|
||||||
import at.eisibaer.jbear2.exception.NoMessageException
|
|
||||||
import at.eisibaer.jbear2.model.BoardEntry
|
|
||||||
import at.eisibaer.jbear2.model.Game
|
|
||||||
import at.eisibaer.jbear2.model.Player
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import at.eisibaer.jbear2.repository.BoardRepository
|
|
||||||
import at.eisibaer.jbear2.repository.GameRepository
|
|
||||||
import at.eisibaer.jbear2.repository.PlayerRepository
|
|
||||||
import at.eisibaer.jbear2.repository.UserRepository
|
|
||||||
import at.eisibaer.jbear2.security.UserDetailsImpl
|
|
||||||
import at.eisibaer.jbear2.service.mapper.GameMapper
|
|
||||||
import at.eisibaer.jbear2.service.mapper.PlayerMapper
|
|
||||||
import at.eisibaer.jbear2.service.mapper.QuestionMapper
|
|
||||||
import at.eisibaer.jbear2.util.Constants.INVITE_CODE_LENGTH
|
|
||||||
import at.eisibaer.jbear2.util.RandomString
|
|
||||||
import jakarta.transaction.Transactional
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.dao.DataIntegrityViolationException
|
|
||||||
import org.springframework.http.ResponseEntity
|
|
||||||
import org.springframework.messaging.handler.annotation.DestinationVariable
|
|
||||||
import org.springframework.messaging.handler.annotation.Header
|
|
||||||
import org.springframework.messaging.handler.annotation.Headers
|
|
||||||
import org.springframework.messaging.handler.annotation.MessageExceptionHandler
|
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping
|
|
||||||
import org.springframework.messaging.handler.annotation.Payload
|
|
||||||
import org.springframework.messaging.handler.annotation.SendTo
|
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate
|
|
||||||
import org.springframework.messaging.simp.annotation.SendToUser
|
|
||||||
import org.springframework.messaging.simp.annotation.SubscribeMapping
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
|
||||||
import org.springframework.stereotype.Controller
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.security.Principal
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.NoSuchElementException
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class GameEndpoint(
|
|
||||||
val playerRepository: PlayerRepository,
|
|
||||||
val userRepository: UserRepository,
|
|
||||||
val boardRepository: BoardRepository,
|
|
||||||
val gameRepository: GameRepository,
|
|
||||||
|
|
||||||
val playerMapper: PlayerMapper,
|
|
||||||
val gameMapper: GameMapper,
|
|
||||||
val questionMapper: QuestionMapper,
|
|
||||||
|
|
||||||
val messagingTemplate: SimpMessagingTemplate,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val log: Logger = LoggerFactory.getLogger(GameEndpoint::class.java);
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@GetMapping("/api/games/{id}")
|
|
||||||
fun getGameByInviteCode(@PathVariable id: String): ResponseEntity<*>{
|
|
||||||
val game: Game;
|
|
||||||
if( id.length == INVITE_CODE_LENGTH){
|
|
||||||
game = gameRepository.findByInviteCode(id) ?: return ResponseEntity.status(404).body("Game with invide code $id not found")
|
|
||||||
} else {
|
|
||||||
var uuid: UUID;
|
|
||||||
try{
|
|
||||||
uuid = UUID.fromString(id)
|
|
||||||
}catch (exception: IllegalArgumentException){
|
|
||||||
return ResponseEntity.status(400).body("Invalid UUID given")
|
|
||||||
}
|
|
||||||
game = gameRepository.findByUuid(uuid) ?: return ResponseEntity.status(404).body("Game with UUID $uuid not found")
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok().body(gameMapper.toDto(game));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@PostMapping("/api/games")
|
|
||||||
fun hostNewGame(@RequestBody body: String?): ResponseEntity<*>{
|
|
||||||
log.info("Start new game");
|
|
||||||
val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl;
|
|
||||||
val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB");
|
|
||||||
|
|
||||||
val firstAvailableBoard = boardRepository.findFirstByOwnerOrderByBoardName(user) ?: return ResponseEntity.status(404).body("No board found for user")
|
|
||||||
|
|
||||||
gameRepository.deleteAll();
|
|
||||||
gameRepository.flush()
|
|
||||||
|
|
||||||
val game = Game(RandomString.ofLength(8), UUID.randomUUID(), firstAvailableBoard, user, emptyList(), emptyList(), null )
|
|
||||||
val savedGame = gameRepository.save(game);
|
|
||||||
return ResponseEntity.ok().body(gameMapper.toDto(savedGame));
|
|
||||||
}
|
|
||||||
|
|
||||||
// @MessageMapping("/join")
|
|
||||||
// fun test(joinMessage: GenericMessage, principal: Principal){
|
|
||||||
// if( joinMessage.content == null ){
|
|
||||||
// messagingTemplate.convertAndSendToUser(principal.name, "/game/joined", GenericMessage("error", "Received empty message"));
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// val game = gameRepository.findByInviteCode(joinMessage.content)
|
|
||||||
// if( game == null) {
|
|
||||||
// messagingTemplate.convertAndSendToUser(principal.name, "/game/joined", GenericMessage("error", "No game found"));
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// val player = Player("test", game);
|
|
||||||
// val payload = GenericMessage(player.name, "${player.name} joined");
|
|
||||||
// messagingTemplate.convertAndSendToUser("TODO ALL", "/game/joined", payload);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @MessageMapping("/host")
|
|
||||||
// fun startGame(message: GenericMessage, principal: Principal ){
|
|
||||||
// log.info("starting to ", principal.name);
|
|
||||||
// val user = userRepository.findUserByUsername(principal.name) ?: throw UsernameNotFoundException("User not found");
|
|
||||||
// val board = boardRepository.findFirstByOwnerOrderByBoardName(user)
|
|
||||||
// if( board == null ){
|
|
||||||
// messagingTemplate.convertAndSendToUser(principal.name, "/game/created", GenericMessage("error", "No Board available for playing!"));
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// val newGame = Game(RandomString.ofLength(8), UUID.randomUUID(), board, user, ArrayList(), ArrayList(), null )
|
|
||||||
// var savedGame: Game;
|
|
||||||
// try{
|
|
||||||
// savedGame = gameRepository.save(newGame);
|
|
||||||
// } catch (dataIntegrityViolationException: DataIntegrityViolationException){
|
|
||||||
// log.error("Could not create game", dataIntegrityViolationException)
|
|
||||||
// messagingTemplate.convertAndSendToUser(principal.name, "/game/created", GenericMessage("error", "Game could not be created!"));
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val payload = GenericMessage("created", savedGame.inviteCode);
|
|
||||||
// messagingTemplate.convertAndSendToUser(principal.name, "/game/created", payload);
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
@MessageMapping("/join/{id}")
|
|
||||||
fun playerJoining(@Payload joinMessage: GenericMessage, @DestinationVariable id: String){
|
|
||||||
val game: Game;
|
|
||||||
if( id.length == INVITE_CODE_LENGTH){
|
|
||||||
game = gameRepository.findByInviteCode(id) ?: throw NoSuchElementException("Game with invide code $id not found")
|
|
||||||
} else {
|
|
||||||
var uuid: UUID;
|
|
||||||
try{
|
|
||||||
uuid = UUID.fromString(id)
|
|
||||||
}catch (exception: IllegalArgumentException){
|
|
||||||
throw IllegalArgumentException("Invalid UUID given")
|
|
||||||
}
|
|
||||||
game = gameRepository.findByUuid(uuid) ?: throw NoSuchElementException("Game with UUID $uuid not found")
|
|
||||||
}
|
|
||||||
val player = Player(joinMessage.content, game);
|
|
||||||
val savedPlayer = playerRepository.save(player)
|
|
||||||
val payload = playerMapper.toDto(savedPlayer);
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/${game.uuid}/joined", payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/start")
|
|
||||||
fun startGame(@Payload startMessage: GenericMessage, @DestinationVariable uuid: UUID){
|
|
||||||
// val game = gameRepository.findByUuid(uuid) ?: throw NoSuchElementException("Game with UUID $uuid not found")
|
|
||||||
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/started", GameMessage(uuid, "The game has been started"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/board/select")
|
|
||||||
fun selectBoard(@Payload boardMessage: GenericMessage, @DestinationVariable uuid: UUID){
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/board/selected", GameMessage(uuid, "The board is being shown"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/boardentry/select")
|
|
||||||
fun selectBoardEntry(@Payload targetGameState: GameStateMessage, @DestinationVariable uuid: UUID){
|
|
||||||
val gameState = GameStateMessage(
|
|
||||||
uuid,
|
|
||||||
targetGameState.categoryIndex,
|
|
||||||
targetGameState.boardEntryIndex,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/boardentry/selected", gameState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/question/layer/select")
|
|
||||||
fun selectQuestionLayer(@Payload targetGameState: GameStateMessage, @DestinationVariable uuid: UUID){
|
|
||||||
val gameState = GameStateMessage(
|
|
||||||
uuid,
|
|
||||||
targetGameState.categoryIndex,
|
|
||||||
targetGameState.boardEntryIndex,
|
|
||||||
targetGameState.questionIndex,
|
|
||||||
targetGameState.questionLayerIndex,
|
|
||||||
);
|
|
||||||
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/questionlayer/selected", gameState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/question/audio/play")
|
|
||||||
fun playAudio(@Payload audioMessage: GenericMessage, @DestinationVariable uuid: UUID){
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/question/audio/playing", GenericMessage("Playing audio"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/question/audio/stop")
|
|
||||||
fun stopAudio(@Payload audioMessage: GenericMessage, @DestinationVariable uuid: UUID){
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/question/audio/stopped", GenericMessage("Playing audio"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/host/game/{uuid}/question/reveal")
|
|
||||||
fun revealQuestion(@Payload gameState: GameStateMessage, @DestinationVariable uuid: UUID){
|
|
||||||
if(
|
|
||||||
gameState.categoryIndex == null ||
|
|
||||||
gameState.boardEntryIndex == null ||
|
|
||||||
gameState.questionIndex == null
|
|
||||||
){
|
|
||||||
throw InvalidPropertiesFormatException("Invalid gameSate to reveal question");
|
|
||||||
}
|
|
||||||
val game = gameRepository.findByUuid(uuid) ?: throw NoSuchElementException("Game with UUID $uuid not found")
|
|
||||||
val board = game.board;
|
|
||||||
|
|
||||||
val question = board.categories[gameState.categoryIndex].boardEntries[gameState.boardEntryIndex].questions[gameState.questionIndex];
|
|
||||||
|
|
||||||
val questionDto = questionMapper.toDto(question);
|
|
||||||
messagingTemplate.convertAndSend("/topic/game/$uuid/question/revealed", questionDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageExceptionHandler
|
|
||||||
@SendToUser("/error")
|
|
||||||
fun handleException(exception: Exception): GenericMessage{
|
|
||||||
log.error("Exception in GameEndpoint", exception)
|
|
||||||
return GenericMessage(exception.message ?: "Unknown error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package at.eisibaer.jbear2.exception
|
|
||||||
|
|
||||||
open class NoMessageException(
|
|
||||||
override val message: String?,
|
|
||||||
override val cause: Throwable? = null,
|
|
||||||
) : RuntimeException(message, cause)
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package at.eisibaer.jbear2.exception
|
|
||||||
|
|
||||||
open class StorageException(
|
|
||||||
override val message: String?,
|
|
||||||
override val cause: Throwable?
|
|
||||||
) : RuntimeException(message, cause)
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package at.eisibaer.jbear2.exception
|
|
||||||
|
|
||||||
class StorageFileNotFoundException(
|
|
||||||
override val message: String?,
|
|
||||||
override val cause: Throwable?
|
|
||||||
) : StorageException(message, cause)
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package at.eisibaer.jbear2.model
|
package at.eisibaer.jbear2.model
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.enums.AnswerType
|
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
|
@ -12,13 +11,9 @@ data class Answer(
|
||||||
@Column(name = "text", nullable = false, unique = false)
|
@Column(name = "text", nullable = false, unique = false)
|
||||||
val text: String,
|
val text: String,
|
||||||
|
|
||||||
@Enumerated
|
|
||||||
@Column(name = "answer_type", nullable = false, unique = false)
|
|
||||||
val answerType: AnswerType = AnswerType.TEXT,
|
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
@JoinColumn(name = "fk_image_file", referencedColumnName = "id")
|
@JoinColumn(name = "fk_image_file", referencedColumnName = "id")
|
||||||
val image: File?,
|
val image: ImageFile?,
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
@JoinColumn(name="fk_board_entry", referencedColumnName = "id")
|
@JoinColumn(name="fk_board_entry", referencedColumnName = "id")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import jakarta.persistence.*
|
||||||
])
|
])
|
||||||
data class Board(
|
data class Board(
|
||||||
|
|
||||||
@OneToMany(mappedBy = "board", cascade = [CascadeType.ALL], orphanRemoval = true)
|
@OneToMany(mappedBy = "board")
|
||||||
val categories: List<Category>,
|
val categories: List<Category>,
|
||||||
|
|
||||||
@Column(name = "board_name", nullable = false, unique = false)
|
@Column(name = "board_name", nullable = false, unique = false)
|
||||||
|
|
@ -18,11 +18,7 @@ data class Board(
|
||||||
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
|
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
|
||||||
val owner: User,
|
val owner: User,
|
||||||
|
|
||||||
@Column(name = "points_are_title", nullable = false, unique = false)
|
|
||||||
val pointsAreTitle: Boolean = false,
|
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,15 @@ data class BoardEntry(
|
||||||
@Column(name = "name", nullable = false, unique = false)
|
@Column(name = "name", nullable = false, unique = false)
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
||||||
@Column(name = "points", nullable = false, unique = false)
|
@OneToMany(mappedBy = "boardEntry")
|
||||||
val points: Long,
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "boardEntry", cascade = [CascadeType.ALL], orphanRemoval = true)
|
|
||||||
val questions: List<Question>,
|
val questions: List<Question>,
|
||||||
|
|
||||||
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
|
@OneToOne
|
||||||
val answer: Answer,
|
val answer: Answer,
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "fk_category", referencedColumnName = "id")
|
@JoinColumn(name = "fk_category", referencedColumnName = "id")
|
||||||
var category: Category?,
|
val category: Category?,
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ data class Category (
|
||||||
@Column(name = "description", nullable = false, unique = false)
|
@Column(name = "description", nullable = false, unique = false)
|
||||||
val description: String,
|
val description: String,
|
||||||
|
|
||||||
@OneToMany(mappedBy = "category", cascade = [CascadeType.ALL], orphanRemoval = true)
|
@OneToMany(mappedBy = "category")
|
||||||
val boardEntries: List<BoardEntry>,
|
val boardEntries: List<BoardEntry>,
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "fk_board", referencedColumnName = "id")
|
@JoinColumn(name = "fk_board", referencedColumnName = "id")
|
||||||
var board: Board?,
|
val board: Board,
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package at.eisibaer.jbear2.model
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
import org.springframework.data.annotation.CreatedDate
|
import org.springframework.data.annotation.CreatedDate
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "games", indexes = [
|
@Table(name = "games", indexes = [
|
||||||
|
|
@ -16,9 +15,6 @@ data class Game(
|
||||||
@Column(name = "invite_code", nullable = false, unique = true)
|
@Column(name = "invite_code", nullable = false, unique = true)
|
||||||
val inviteCode: String,
|
val inviteCode: String,
|
||||||
|
|
||||||
@Column(name = "uuid", nullable = false, unique = true)
|
|
||||||
val uuid: UUID,
|
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "fk_board", referencedColumnName = "id")
|
@JoinColumn(name = "fk_board", referencedColumnName = "id")
|
||||||
val board: Board,
|
val board: Board,
|
||||||
|
|
@ -27,9 +23,13 @@ data class Game(
|
||||||
@JoinColumn(name="fk_host_user", referencedColumnName = "id")
|
@JoinColumn(name="fk_host_user", referencedColumnName = "id")
|
||||||
val host: User,
|
val host: User,
|
||||||
|
|
||||||
@OneToMany(mappedBy = "currentGame", cascade = [CascadeType.ALL], orphanRemoval = true)
|
@OneToMany(mappedBy = "currentGame")
|
||||||
val players: List<Player>,
|
val players: List<Player>,
|
||||||
|
|
||||||
|
@OneToOne
|
||||||
|
@JoinColumn(name="fk_player_currently_choosing", referencedColumnName = "id")
|
||||||
|
val currentlyChoosingPlayer: Player,
|
||||||
|
|
||||||
@ManyToMany
|
@ManyToMany
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "answered_board_entries_in_games",
|
name = "answered_board_entries_in_games",
|
||||||
|
|
@ -38,16 +38,12 @@ data class Game(
|
||||||
)
|
)
|
||||||
val alreadyAnsweredEntries: List<BoardEntry>,
|
val alreadyAnsweredEntries: List<BoardEntry>,
|
||||||
|
|
||||||
@OneToOne
|
@Column(name = "accepting_answers", nullable = false, unique = false)
|
||||||
@JoinColumn(name="fk_player_currently_choosing", referencedColumnName = "id")
|
val acceptingAnswers: Boolean? = false,
|
||||||
val currentlyChoosingPlayer: Player?,
|
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
@Column(name = "created_timestamp", nullable = false, unique = false)
|
@Column(name = "created_timestamp", nullable = false, unique = false)
|
||||||
val createdTimestamp: Instant? = Instant.now(),
|
val createdTimestamp: Instant,
|
||||||
|
|
||||||
@Column(name = "accepting_answers", nullable = false, unique = false)
|
|
||||||
val acceptingAnswers: Boolean? = false,
|
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import java.util.UUID
|
||||||
@Table(name = "image_files", indexes = [
|
@Table(name = "image_files", indexes = [
|
||||||
Index(name = "fk_owner_to_image_file", columnList = "fk_owned_by")
|
Index(name = "fk_owner_to_image_file", columnList = "fk_owned_by")
|
||||||
])
|
])
|
||||||
data class File (
|
data class ImageFile (
|
||||||
|
|
||||||
@Column(name = "uuid", nullable = false, unique = true)
|
@Column(name = "uuid", nullable = false, unique = true)
|
||||||
val uuid: UUID,
|
val uuid: UUID,
|
||||||
|
|
@ -22,9 +22,6 @@ data class File (
|
||||||
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
|
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
|
||||||
val owner: User,
|
val owner: User,
|
||||||
|
|
||||||
@Column(name = "file_type", nullable = false, unique = false)
|
|
||||||
val fileType: FileType,
|
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
val question: Question?,
|
val question: Question?,
|
||||||
|
|
||||||
|
|
@ -35,9 +32,3 @@ data class File (
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class FileType{
|
|
||||||
IMAGE,
|
|
||||||
AUDIO,
|
|
||||||
PROFILE_PICTURE,
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package at.eisibaer.jbear2.model
|
package at.eisibaer.jbear2.model
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.enums.QuestionType
|
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
|
@ -14,20 +13,17 @@ data class Question(
|
||||||
@Column(name = "text", nullable = false, unique = false)
|
@Column(name = "text", nullable = false, unique = false)
|
||||||
val text: String,
|
val text: String,
|
||||||
|
|
||||||
@Enumerated
|
@ManyToOne
|
||||||
@Column(name = "question_type", nullable = false, unique = false)
|
@JoinColumn(name = "fk_question_type", referencedColumnName = "id")
|
||||||
val questionType: QuestionType = QuestionType.TEXT,
|
val questionType: QuestionType,
|
||||||
|
|
||||||
@Column(name = "font_scaling", nullable = false, unique = false)
|
|
||||||
val fontScaling: Int,
|
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
@JoinColumn(name = "fk_image", referencedColumnName = "id")
|
@JoinColumn(name = "fk_image", referencedColumnName = "id")
|
||||||
val image: File?,
|
val image: ImageFile?,
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "fk_board_entry", referencedColumnName = "id")
|
@JoinColumn(name = "fk_board_entry", referencedColumnName = "id")
|
||||||
var boardEntry: BoardEntry?,
|
val boardEntry: BoardEntry?,
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package at.eisibaer.jbear2.model
|
||||||
|
|
||||||
|
import jakarta.persistence.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "question_types")
|
||||||
|
data class QuestionType(
|
||||||
|
|
||||||
|
@Column(name = "title", nullable = false, unique = true)
|
||||||
|
val title: String,
|
||||||
|
|
||||||
|
@Column(name = "description", nullable = false, unique = true)
|
||||||
|
val description: String,
|
||||||
|
|
||||||
|
@Column(name = "active", nullable = false, unique = false)
|
||||||
|
val active: Boolean,
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
|
val id: Long? = null,
|
||||||
|
)
|
||||||
|
|
@ -20,7 +20,7 @@ data class User(
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
@JoinColumn(name = "fk_profile_picture", referencedColumnName = "id")
|
@JoinColumn(name = "fk_profile_picture", referencedColumnName = "id")
|
||||||
var profilePicture: File?,
|
var profilePicture: ImageFile?,
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package at.eisibaer.jbear2.model.enums
|
|
||||||
|
|
||||||
enum class AnswerType {
|
|
||||||
TEXT,
|
|
||||||
IMAGE,
|
|
||||||
AUDIO,
|
|
||||||
LOCATION;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package at.eisibaer.jbear2.model.enums
|
|
||||||
|
|
||||||
enum class QuestionType {
|
|
||||||
TEXT,
|
|
||||||
IMAGE,
|
|
||||||
AUDIO,
|
|
||||||
LOCATION;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package at.eisibaer.jbear2.repository
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.Board
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface BoardRepository : JpaRepository<Board, Long> {
|
|
||||||
|
|
||||||
fun findByIdAndOwner(id: Long, user: User): Board?
|
|
||||||
|
|
||||||
fun findAllByOwner(owner: User): List<Board>
|
|
||||||
|
|
||||||
fun findFirstByOwnerOrderByBoardName(owner: User): Board?
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package at.eisibaer.jbear2.repository
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface FileRepository : JpaRepository<File, Long> {
|
|
||||||
|
|
||||||
fun findByUuid(uuid: UUID): File?;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package at.eisibaer.jbear2.repository
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.Game
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
interface GameRepository : JpaRepository<Game, Long> {
|
|
||||||
|
|
||||||
fun findByInviteCode(inviteCode: String): Game?
|
|
||||||
|
|
||||||
fun findByUuid(uuid: UUID): Game?
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package at.eisibaer.jbear2.repository
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.Player
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
|
|
||||||
interface PlayerRepository : JpaRepository<Player, Long> {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,7 @@ import java.util.*
|
||||||
@Repository
|
@Repository
|
||||||
interface UserRepository : JpaRepository<User, Long> {
|
interface UserRepository : JpaRepository<User, Long> {
|
||||||
|
|
||||||
fun findUserByUsername(username: String): User?;
|
fun findUserByUsername(username: String): Optional<User>;
|
||||||
|
|
||||||
fun existsByUsername(username: String): Boolean;
|
fun existsByUsername(username: String): Boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ class SecurityConfiguration(
|
||||||
return addCsrfConfig(httpSecurity)
|
return addCsrfConfig(httpSecurity)
|
||||||
.authorizeHttpRequests { config ->
|
.authorizeHttpRequests { config ->
|
||||||
config
|
config
|
||||||
|
.requestMatchers("/api/auth/*").permitAll()
|
||||||
.requestMatchers("/api/user/**").authenticated()
|
.requestMatchers("/api/user/**").authenticated()
|
||||||
.requestMatchers("/**").permitAll()
|
.requestMatchers("/**").permitAll()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,7 @@ class UserDetailServiceImpl(
|
||||||
): UserDetailsService {
|
): UserDetailsService {
|
||||||
|
|
||||||
override fun loadUserByUsername(username: String?): UserDetailsImpl {
|
override fun loadUserByUsername(username: String?): UserDetailsImpl {
|
||||||
val user: User? = userRepository.findUserByUsername( username ?: "" )
|
val user: User = userRepository.findUserByUsername( username ?: "" ).orElseThrow { UsernameNotFoundException("User not found by username \"$username\"") }
|
||||||
|
|
||||||
if( user == null ){
|
|
||||||
throw UsernameNotFoundException("User not found by username \"$username\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserDetailsImpl(
|
return UserDetailsImpl(
|
||||||
user.id!!,
|
user.id!!,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.BoardDto
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import org.springframework.http.ResponseEntity
|
|
||||||
|
|
||||||
interface BoardService {
|
|
||||||
|
|
||||||
fun getBoardsByUser( user: User ): ResponseEntity<List<BoardDto>?>
|
|
||||||
|
|
||||||
fun getBoardByUserAndId( user: User, boardId: Long ): ResponseEntity<BoardDto?>
|
|
||||||
|
|
||||||
fun saveBoardToUser( user: User, boardDto: BoardDto ): ResponseEntity<BoardDto>
|
|
||||||
fun deleteBoardOfUser( user: User, boardId: Long ): ResponseEntity<Unit>
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.BoardDto
|
|
||||||
import at.eisibaer.jbear2.model.Board
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import at.eisibaer.jbear2.repository.BoardRepository
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import at.eisibaer.jbear2.repository.UserRepository
|
|
||||||
import at.eisibaer.jbear2.service.mapper.BoardMapper
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.http.ResponseEntity
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class BoardServiceImpl (
|
|
||||||
private val boardRepository: BoardRepository,
|
|
||||||
private val userRepository: UserRepository,
|
|
||||||
private val fileRepository: FileRepository,
|
|
||||||
private val boardMapper: BoardMapper,
|
|
||||||
) : BoardService {
|
|
||||||
|
|
||||||
val log: Logger = LoggerFactory.getLogger(BoardServiceImpl::class.java);
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
override fun getBoardsByUser(user: User): ResponseEntity<List<BoardDto>?> {
|
|
||||||
val boards = boardRepository.findAllByOwner(user)
|
|
||||||
return ResponseEntity.ok(boardMapper.toDto(boards));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
override fun getBoardByUserAndId(user: User, boardId: Long): ResponseEntity<BoardDto?> {
|
|
||||||
val board = boardRepository.findByIdAndOwner(boardId, user) ?: return ResponseEntity.status(404).build();
|
|
||||||
|
|
||||||
return ResponseEntity.ok(boardMapper.toDto(board))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
override fun saveBoardToUser( user: User, boardDto: BoardDto): ResponseEntity<BoardDto> {
|
|
||||||
val board: Board = boardMapper.toEntity(boardDto, user, userRepository, fileRepository);
|
|
||||||
|
|
||||||
val savedBoard: Board;
|
|
||||||
try {
|
|
||||||
savedBoard = boardRepository.save(board);
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
log.error(ex.message, ex);
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(boardMapper.toDto(savedBoard))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteBoardOfUser(user: User, boardId: Long): ResponseEntity<Unit> {
|
|
||||||
boardRepository.deleteById( boardId );
|
|
||||||
|
|
||||||
//TODO Delete Images
|
|
||||||
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import org.springframework.core.io.Resource
|
|
||||||
import org.springframework.web.multipart.MultipartFile
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
interface FileService {
|
|
||||||
|
|
||||||
fun saveFiles(files: List<MultipartFile>): List<File>;
|
|
||||||
fun saveFile(file: MultipartFile): File;
|
|
||||||
|
|
||||||
fun getFile(file: File): Resource;
|
|
||||||
fun getFile(fileUUID: UUID): Resource;
|
|
||||||
|
|
||||||
fun deleteFile(file: File): Resource;
|
|
||||||
fun deleteFile(fileUUID: UUID): Resource;
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import org.springframework.core.io.Resource
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import org.springframework.web.multipart.MultipartFile
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class FileServiceImpl(
|
|
||||||
val fileRepository: FileRepository,
|
|
||||||
) : FileService {
|
|
||||||
|
|
||||||
override fun saveFiles(files: List<MultipartFile>): List<File> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveFile(file: MultipartFile): File {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveMultipartFile(file: MultipartFile): File{
|
|
||||||
val uuid = UUID.randomUUID();
|
|
||||||
val filename = file.originalFilename;
|
|
||||||
var hash: String;
|
|
||||||
// val file = File(
|
|
||||||
// UUID.randomUUID(),
|
|
||||||
// uuidextension,
|
|
||||||
// hash,
|
|
||||||
// )
|
|
||||||
TODO();
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFile(file: File): Resource {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFile(fileUUID: UUID): Resource {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteFile(file: File): Resource {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteFile(fileUUID: UUID): Resource {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.config.ApplicationProperties
|
|
||||||
import at.eisibaer.jbear2.exception.StorageException
|
|
||||||
import at.eisibaer.jbear2.exception.StorageFileNotFoundException
|
|
||||||
import jakarta.annotation.PostConstruct
|
|
||||||
import org.springframework.core.io.Resource
|
|
||||||
import org.springframework.core.io.UrlResource
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import org.springframework.transaction.annotation.Propagation
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
import org.springframework.util.FileSystemUtils
|
|
||||||
import org.springframework.web.multipart.MultipartFile
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.MalformedURLException
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Transactional(propagation = Propagation.NEVER)
|
|
||||||
class FileSystemStorageService(
|
|
||||||
val applicationProperties: ApplicationProperties,
|
|
||||||
) : StorageService {
|
|
||||||
|
|
||||||
lateinit var rootLocation: Path
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
fun init(){
|
|
||||||
val location = applicationProperties.storage.fs.location;
|
|
||||||
if( location.trim().isEmpty() ){
|
|
||||||
throw StorageException("File upload location can not be Empty", null)
|
|
||||||
}
|
|
||||||
rootLocation = Paths.get(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun storeFile(file: MultipartFile) {
|
|
||||||
storeMultipartFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun storeFiles(files: List<MultipartFile>) {
|
|
||||||
for( file in files ){
|
|
||||||
storeMultipartFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFile(filename: String): Resource {
|
|
||||||
try {
|
|
||||||
val file: Path = rootLocation.resolve(filename)
|
|
||||||
val resource: Resource = UrlResource(file.toUri())
|
|
||||||
if (resource.exists() || resource.isReadable) {
|
|
||||||
return resource
|
|
||||||
} else {
|
|
||||||
throw StorageFileNotFoundException("Could not read file: $filename", null)
|
|
||||||
}
|
|
||||||
} catch (e: MalformedURLException) {
|
|
||||||
throw StorageFileNotFoundException("Could not read file: $filename", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteFile(filename: String) {
|
|
||||||
FileSystemUtils.deleteRecursively(rootLocation.resolve(filename))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun storeMultipartFile(file: MultipartFile){
|
|
||||||
try {
|
|
||||||
if (file.isEmpty) {
|
|
||||||
throw StorageException("Failed to store empty file.", null)
|
|
||||||
}
|
|
||||||
val destinationFile: Path = rootLocation
|
|
||||||
.resolve(Paths.get(file.originalFilename))
|
|
||||||
.normalize()
|
|
||||||
.toAbsolutePath()
|
|
||||||
if (destinationFile.getParent() != rootLocation.toAbsolutePath()) {
|
|
||||||
// This is a security check
|
|
||||||
throw StorageException("Cannot store file outside current directory.", null)
|
|
||||||
}
|
|
||||||
file.inputStream.use { inputStream ->
|
|
||||||
Files.copy(
|
|
||||||
inputStream, destinationFile,
|
|
||||||
StandardCopyOption.REPLACE_EXISTING
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw StorageException("Failed to store file.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service
|
|
||||||
|
|
||||||
import org.springframework.core.io.Resource
|
|
||||||
import org.springframework.web.multipart.MultipartFile
|
|
||||||
|
|
||||||
interface StorageService {
|
|
||||||
|
|
||||||
fun storeFile(file: MultipartFile)
|
|
||||||
|
|
||||||
fun storeFiles(files: List<MultipartFile>)
|
|
||||||
|
|
||||||
fun getFile(filename: String): Resource
|
|
||||||
|
|
||||||
fun deleteFile(filename: String)
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.AnswerDto
|
|
||||||
import at.eisibaer.jbear2.model.Answer
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import at.eisibaer.jbear2.model.enums.AnswerType
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import org.mapstruct.Context
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
abstract class AnswerMapper {
|
|
||||||
|
|
||||||
abstract fun toDto(e: Answer): AnswerDto
|
|
||||||
abstract fun toDto(e: List<Answer>): List<AnswerDto>
|
|
||||||
|
|
||||||
abstract fun toEntity(d: AnswerDto, @Context fileRepository: FileRepository): Answer
|
|
||||||
abstract fun toEntity(d: List<AnswerDto>, @Context fileRepository: FileRepository): List<Answer>
|
|
||||||
|
|
||||||
fun map(file: File?): String{
|
|
||||||
return file?.uuid.toString()
|
|
||||||
}
|
|
||||||
fun map(imageUuid: String?, @Context fileRepository: FileRepository): File? {
|
|
||||||
return if( imageUuid == null ) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
try{
|
|
||||||
fileRepository.findByUuid(UUID.fromString(imageUuid))
|
|
||||||
} catch ( illegalArgumentException: IllegalArgumentException ){
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun map(type: AnswerType): Int {
|
|
||||||
return type.ordinal;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun map(value: Int): AnswerType {
|
|
||||||
return AnswerType.entries[value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.BoardEntryDto
|
|
||||||
import at.eisibaer.jbear2.model.BoardEntry
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import org.mapstruct.AfterMapping
|
|
||||||
import org.mapstruct.Context
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import org.mapstruct.MappingTarget
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Mapper(uses = [QuestionMapper::class,AnswerMapper::class])
|
|
||||||
abstract class BoardEntryMapper {
|
|
||||||
|
|
||||||
abstract fun toDto(e: BoardEntry): BoardEntryDto;
|
|
||||||
abstract fun toDto(e: List<BoardEntry>): List<BoardEntryDto>;
|
|
||||||
|
|
||||||
abstract fun toEntity(d: BoardEntryDto, @Context fileRepository: FileRepository): BoardEntry;
|
|
||||||
abstract fun toEntity(d: List<BoardEntryDto>, @Context fileRepository: FileRepository): List<BoardEntry>;
|
|
||||||
|
|
||||||
fun map(file: File): String{
|
|
||||||
return file.uuid.toString()
|
|
||||||
}
|
|
||||||
fun map(imageUuid: String?, @Context fileRepository: FileRepository): File? {
|
|
||||||
return if( imageUuid == null ) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
fileRepository.findByUuid(UUID.fromString(imageUuid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterMapping
|
|
||||||
fun addBoardEntryToQuestion(source: BoardEntryDto, @MappingTarget target: BoardEntry): BoardEntry {
|
|
||||||
for( question in target.questions ){
|
|
||||||
question.boardEntry = target
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.BoardDto
|
|
||||||
import at.eisibaer.jbear2.model.Board
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import at.eisibaer.jbear2.repository.UserRepository
|
|
||||||
import org.mapstruct.*
|
|
||||||
|
|
||||||
@Mapper(uses = [UserMapper::class,QuestionMapper::class,AnswerMapper::class,BoardEntryMapper::class,CategoryMapper::class])
|
|
||||||
abstract class BoardMapper {
|
|
||||||
|
|
||||||
abstract fun toDto(e: Board): BoardDto;
|
|
||||||
abstract fun toDto(e: List<Board>): List<BoardDto>;
|
|
||||||
|
|
||||||
@Mapping(target = "owner", source = "owner")
|
|
||||||
@Mapping(target = "id", source = "d.id")
|
|
||||||
abstract fun toEntity(d: BoardDto, owner: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): Board;
|
|
||||||
abstract fun toEntity(d: List<BoardDto>, @Context owner: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): List<Board>;
|
|
||||||
|
|
||||||
@AfterMapping
|
|
||||||
fun addBoardToCategory(source: BoardDto, @MappingTarget target: Board ): Board{
|
|
||||||
for( category in target.categories ){
|
|
||||||
category.board = target
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.CategoryDto
|
|
||||||
import at.eisibaer.jbear2.model.Category
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import org.mapstruct.AfterMapping
|
|
||||||
import org.mapstruct.Context
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import org.mapstruct.MappingTarget
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Mapper(uses = [QuestionMapper::class,AnswerMapper::class,BoardEntryMapper::class])
|
|
||||||
abstract class CategoryMapper {
|
|
||||||
|
|
||||||
abstract fun toDto(e: Category): CategoryDto;
|
|
||||||
abstract fun toDto(e: List<Category>): List<CategoryDto>;
|
|
||||||
|
|
||||||
abstract fun toEntity(d: CategoryDto, @Context fileRepository: FileRepository): Category;
|
|
||||||
abstract fun toEntity(d: List<CategoryDto>, @Context fileRepository: FileRepository): List<Category>;
|
|
||||||
|
|
||||||
fun map(file: File): String{
|
|
||||||
return file.uuid.toString()
|
|
||||||
}
|
|
||||||
fun map(imageUuid: String?, @Context fileRepository: FileRepository): File? {
|
|
||||||
return if( imageUuid == null ) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
fileRepository.findByUuid(UUID.fromString(imageUuid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterMapping
|
|
||||||
fun addCategoryToBoardEntry(source: CategoryDto, @MappingTarget target: Category): Category {
|
|
||||||
for( boardEntry in target.boardEntries ){
|
|
||||||
boardEntry.category = target
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
interface EntityMapper<D, E> {
|
|
||||||
fun toDto(e: E): D;
|
|
||||||
fun toDto(e: List<E>): List<D>;
|
|
||||||
|
|
||||||
fun toEntity(d: D): E;
|
|
||||||
fun toEntity(d: List<D>): List<E>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.game.GameDto
|
|
||||||
import at.eisibaer.jbear2.model.BoardEntry
|
|
||||||
import at.eisibaer.jbear2.model.Game
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
interface GameMapper : EntityMapper<GameDto, Game> {
|
|
||||||
//
|
|
||||||
// fun map(boardEntry: BoardEntry): Long{
|
|
||||||
// return boardEntry.id!!
|
|
||||||
// }
|
|
||||||
// fun map(boardEntries: List<BoardEntry>): List<Long>{
|
|
||||||
// return boardEntries.map { it.id!! }
|
|
||||||
// }
|
|
||||||
// fun map(ids: List<Long>): List<BoardEntry>{
|
|
||||||
// return emptyList()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import org.mapstruct.Context
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
abstract class ImageMapper {
|
|
||||||
|
|
||||||
fun toEntity(d: String, @Context fileRepository: FileRepository): File? {
|
|
||||||
return fileRepository.findByUuid(UUID.fromString(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toDto(e: File): String {
|
|
||||||
return e.uuid.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toDto(e: List<File>): List<String>{
|
|
||||||
return e.map { toDto(it) }
|
|
||||||
}
|
|
||||||
fun toEntity(d: List<String>, @Context fileRepository: FileRepository): List<File?>{
|
|
||||||
return d.map { toEntity(it, fileRepository) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.game.PlayerDto
|
|
||||||
import at.eisibaer.jbear2.model.Player
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
interface PlayerMapper : EntityMapper<PlayerDto, Player> {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.board.QuestionDto
|
|
||||||
import at.eisibaer.jbear2.model.File
|
|
||||||
import at.eisibaer.jbear2.model.Question
|
|
||||||
import at.eisibaer.jbear2.model.enums.QuestionType
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import org.mapstruct.Context
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
abstract class QuestionMapper {
|
|
||||||
|
|
||||||
abstract fun toDto(e: Question): QuestionDto;
|
|
||||||
abstract fun toDto(e: List<Question>): List<QuestionDto>;
|
|
||||||
|
|
||||||
abstract fun toEntity(d: QuestionDto, @Context fileRepository: FileRepository): Question;
|
|
||||||
abstract fun toEntity(d: List<QuestionDto>, @Context fileRepository: FileRepository): List<Question>;
|
|
||||||
|
|
||||||
fun map(file: File?): String?{
|
|
||||||
return file?.uuid.toString();
|
|
||||||
}
|
|
||||||
fun map(imageUuid: String?, @Context fileRepository: FileRepository): File? {
|
|
||||||
return if( imageUuid == null ) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
try{
|
|
||||||
fileRepository.findByUuid(UUID.fromString(imageUuid))
|
|
||||||
} catch ( illegalArgumentException: IllegalArgumentException ){
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun map(type: QuestionType): Int {
|
|
||||||
return type.ordinal;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun map(value: Int): QuestionType {
|
|
||||||
return QuestionType.entries[value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package at.eisibaer.jbear2.service.mapper
|
|
||||||
|
|
||||||
import at.eisibaer.jbear2.dto.user.UserDto
|
|
||||||
import at.eisibaer.jbear2.model.User
|
|
||||||
import at.eisibaer.jbear2.repository.FileRepository
|
|
||||||
import at.eisibaer.jbear2.repository.UserRepository
|
|
||||||
import org.mapstruct.Context
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
|
||||||
|
|
||||||
@Mapper(uses = [ImageMapper::class, UserMapper::class])
|
|
||||||
interface UserMapper{
|
|
||||||
|
|
||||||
fun toDto(e: User): UserDto;
|
|
||||||
fun toDto(e: List<User>): List<UserDto>;
|
|
||||||
|
|
||||||
fun toEntity(d: UserDto, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): User;
|
|
||||||
fun toEntity(d: List<UserDto>, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): List<User>;
|
|
||||||
|
|
||||||
fun map(e: User): String{
|
|
||||||
return e.username;
|
|
||||||
}
|
|
||||||
fun map(d: String, @Context userRepository: UserRepository): User{
|
|
||||||
val user = userRepository.findUserByUsername(d)
|
|
||||||
?: throw UsernameNotFoundException("User not found by username \"$d\"")
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -2,5 +2,4 @@ package at.eisibaer.jbear2.util
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val STR_SESSION_USER_KEY = "user"
|
const val STR_SESSION_USER_KEY = "user"
|
||||||
const val INVITE_CODE_LENGTH = 8
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package at.eisibaer.jbear2.util
|
|
||||||
|
|
||||||
import java.security.SecureRandom
|
|
||||||
|
|
||||||
|
|
||||||
class RandomString{
|
|
||||||
companion object{
|
|
||||||
val AB: String = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
val rnd: SecureRandom = SecureRandom()
|
|
||||||
|
|
||||||
fun ofLength(len: Int): String {
|
|
||||||
val sb = StringBuilder(len)
|
|
||||||
for (i in 0 until len) sb.append(AB[rnd.nextInt(AB.length)])
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,9 +3,6 @@ logging:
|
||||||
at:
|
at:
|
||||||
eisibaer:
|
eisibaer:
|
||||||
jbear2: "DEBUG"
|
jbear2: "DEBUG"
|
||||||
# org:
|
|
||||||
# springframework:
|
|
||||||
# security: "TRACE"
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://${PG_HOST}:${PG_PORT}/jeobeardy?currentSchema=jeobeardy-app
|
url: jdbc:postgresql://${PG_HOST}:${PG_PORT}/jeobeardy?currentSchema=jeobeardy-app
|
||||||
|
|
|
||||||
|
|
@ -14,29 +14,10 @@ spring:
|
||||||
docker:
|
docker:
|
||||||
compose:
|
compose:
|
||||||
lifecycle-management: start-only
|
lifecycle-management: start-only
|
||||||
servlet:
|
|
||||||
multipart:
|
|
||||||
max-file-size: 5MB
|
|
||||||
max-request-size: 50MB
|
|
||||||
# session:
|
|
||||||
# redis:
|
|
||||||
# flush-mode: on_save
|
|
||||||
# namespace: spring:session
|
|
||||||
# data:
|
|
||||||
# redis:
|
|
||||||
# host: localhost
|
|
||||||
# password:
|
|
||||||
# port: 6379
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
address: localhost
|
address: localhost
|
||||||
port: 8008
|
port: 8008
|
||||||
# servlet:
|
|
||||||
# session:
|
|
||||||
# timeout:
|
|
||||||
|
|
||||||
application:
|
application:
|
||||||
cors-allowed-methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
cors-allowed-methods: ["GET", "POST", "DELETE", "OPTIONS"]
|
||||||
storage:
|
|
||||||
fs:
|
|
||||||
location: "files"
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vite App</title>
|
<title>Vite App</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B_AXNEWQ.js"></script>
|
<script type="module" crossorigin src="/assets/index-VvMfePyX.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B5vQMdQB.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-21nzev1V.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* 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'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { defineConfig } from "eslint/config";
|
|
||||||
import { js } from "@eslint/js";
|
|
||||||
import { FlagCompat } from "eslint/eslintrc";
|
|
||||||
import pluginVue from "eslint-plugin-vue";
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
recommendedConfig: js.configs.recommended,
|
|
||||||
allConfig: js.configs.all
|
|
||||||
});
|
|
||||||
|
|
||||||
export default defineConfig([{
|
|
||||||
...pluginVue.configs['flat/essential'],
|
|
||||||
extends: compat.extends(
|
|
||||||
"plugin:vue/vue3-essential",
|
|
||||||
"eslint:recommended",
|
|
||||||
"@vue/eslint-config-typescript",
|
|
||||||
),
|
|
||||||
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
parserOptions: {},
|
|
||||||
},
|
|
||||||
}]);
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,37 +13,39 @@
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "~7.2.0",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/vue-fontawesome": "~3.1.3",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
"@stomp/stompjs": "~7.3.0",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@vuelidate/core": "~2.0.3",
|
"@vuelidate/core": "^2.0.3",
|
||||||
"@vuelidate/validators": "~2.0.4",
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"@vueuse/core": "~14.2.1",
|
"axios": "^1.7.2",
|
||||||
"axios": "~1.13.6",
|
"bootstrap": "^5.3.3",
|
||||||
"bootstrap": "~5.3.8",
|
"pinia": "^2.1.7",
|
||||||
"pinia": "~3.0.4",
|
"vue": "^3.4.21",
|
||||||
"vue": "~3.5.30",
|
"vue-draggable-plus": "^0.5.3",
|
||||||
"vue-draggable-plus": "~0.6.1",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-i18n": "~11.3.0",
|
"vue-router": "^4.3.0"
|
||||||
"vue-router": "~5.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "~10.0.1",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@intlify/eslint-plugin-vue-i18n": "~4.3.0",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@tsconfig/node24": "~24.0.4",
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/bootstrap": "~5.2.10",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@types/node": "~25.5.0",
|
"@types/node": "^20.12.5",
|
||||||
"@vitejs/plugin-vue": "~6.0.5",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-typescript": "~14.7.0",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
"@vue/test-utils": "~2.4.6",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"@vue/tsconfig": "~0.9.0",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"eslint": "~10.0.3",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "~10.8.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"sass": "~1.98.0",
|
"jsdom": "^24.0.0",
|
||||||
"typescript": "~5.9.3",
|
"npm-run-all2": "^6.1.2",
|
||||||
"vite": "~8.0.0",
|
"sass": "^1.77.6",
|
||||||
"vite-plugin-vue-devtools": "~8.1.0",
|
"typescript": "~5.4.0",
|
||||||
"vitest": "~4.1.0"
|
"vite": "^5.2.8",
|
||||||
|
"vite-plugin-vue-devtools": "^7.0.25",
|
||||||
|
"vitest": "^1.4.0",
|
||||||
|
"vue-tsc": "^2.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,28 @@ import { useUserStore } from './stores/UserStore';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const userLoading = ref(true);
|
const userLoading = ref( true );
|
||||||
userStore.userCheckPromise.finally(() => {
|
userStore.userCheckPromise
|
||||||
|
.finally( () => {
|
||||||
userLoading.value = false;
|
userLoading.value = false;
|
||||||
});
|
} );
|
||||||
|
|
||||||
const infoModal = ref<InstanceType<typeof GenericInfoModal> | null>(null);
|
const infoModal = ref<InstanceType<typeof GenericInfoModal> | null>( null );
|
||||||
|
|
||||||
function showInfoModal(title: string, text: string): void {
|
function showInfoModal( title: string, text: string ): void {
|
||||||
if (infoModal.value) {
|
if( infoModal.value ) {
|
||||||
infoModal.value.modalTitle = title;
|
infoModal.value.modalTitle = title;
|
||||||
infoModal.value.modalText = text;
|
infoModal.value.modalText = text;
|
||||||
infoModal.value.show();
|
infoModal.value.show();
|
||||||
} else {
|
} else {
|
||||||
console.error('Modal not yet available');
|
console.error( 'Modal not yet available' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provide(infoModalShowFnKey, showInfoModal);
|
provide( infoModalShowFnKey, showInfoModal );
|
||||||
|
|
||||||
const navbar = ref<InstanceType<typeof NavBar> | undefined>(undefined);
|
const navbar = ref<InstanceType<typeof NavBar> | undefined>(undefined);
|
||||||
provide(navbarKey, navbar);
|
provide( navbarKey, navbar);
|
||||||
|
|
||||||
userStore.initialHistoryLen = window.history.length;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,3 @@
|
||||||
.cursor-move{
|
.cursor-move{
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinny-spin{
|
|
||||||
animation-name: spinny;
|
|
||||||
animation-duration: 1.5s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
}
|
|
||||||
@keyframes spinny {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB |
|
|
@ -55,8 +55,6 @@ $breadcrumb-divider: quote(">");
|
||||||
$form-file-button-bg: var(--#{$prefix}secondary-bg);
|
$form-file-button-bg: var(--#{$prefix}secondary-bg);
|
||||||
$form-file-button-hover-bg: var(--#{$prefix}tertiary-bg);
|
$form-file-button-hover-bg: var(--#{$prefix}tertiary-bg);
|
||||||
|
|
||||||
// $btn-border-radius: 0;
|
|
||||||
// $card-border-radius: 0;
|
|
||||||
|
|
||||||
/* Bootstrap Color Map adjustments */
|
/* Bootstrap Color Map adjustments */
|
||||||
$custom-colors: (
|
$custom-colors: (
|
||||||
|
|
@ -136,24 +134,3 @@ $utilities: map-merge(
|
||||||
@import "./exotic_theme.scss";
|
@import "./exotic_theme.scss";
|
||||||
|
|
||||||
@import "../css/main.css";
|
@import "../css/main.css";
|
||||||
|
|
||||||
@mixin animate($animation,$duration,$function,$times,$direction){
|
|
||||||
animation-name: $animation;
|
|
||||||
animation-duration: $duration;
|
|
||||||
animation-iteration-count: $times;
|
|
||||||
animation-timing-function: $function;
|
|
||||||
animation-direction: $direction;
|
|
||||||
}
|
|
||||||
@mixin keyframes($name){
|
|
||||||
@keyframes #{$name}{
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-pulse-primary{
|
|
||||||
@include keyframes(pulse){
|
|
||||||
to {
|
|
||||||
background-color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@include animate(pulse, .75s, ease, infinite, alternate-reverse);
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BoardEntry } from '@/models/board/BoardEntry';
|
import type { BoardEntry } from '@/models/board/BoardEntry';
|
||||||
import { computed, ref } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const QUESTION_TYPE_SIMPLE_TEXT_ID = 1;
|
const QUESTION_TYPE_SIMPLE_TEXT_ID = 1;
|
||||||
const QUESTION_TYPE_IMAGE_ID = 2;
|
const QUESTION_TYPE_IMAGE_ID = 2;
|
||||||
const QUESTION_TYPE_AUDIO_ID = 3;
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
boardEntry: BoardEntry,
|
boardEntry: BoardEntry,
|
||||||
selectedQuestionIndex: number,
|
selectedQuestionIndex: number,
|
||||||
isAnswerShown: boolean,
|
isAnswerShown: boolean,
|
||||||
isQuestionShown: boolean,
|
|
||||||
categoryName: string,
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
questionSelected: [questionIndex: number],
|
questionSelected: [questionIndex: number],
|
||||||
showQuestion: [],
|
|
||||||
hideQuestion: [],
|
|
||||||
showAnswer: [],
|
|
||||||
hideAnswer: [],
|
|
||||||
showBoard: [],
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const question = computed( () => {
|
const question = computed( () => {
|
||||||
|
|
@ -31,37 +23,6 @@ function selectQuestionIndex( qIndex: number ) {
|
||||||
emit( "questionSelected", qIndex );
|
emit( "questionSelected", qIndex );
|
||||||
}
|
}
|
||||||
|
|
||||||
function showQuestion(){
|
|
||||||
emit("showQuestion");
|
|
||||||
}
|
|
||||||
function hideQuestion(){
|
|
||||||
emit("hideQuestion");
|
|
||||||
}
|
|
||||||
function showAnswer(){
|
|
||||||
emit("showAnswer");
|
|
||||||
}
|
|
||||||
function hideAnswer(){
|
|
||||||
emit("hideAnswer");
|
|
||||||
}
|
|
||||||
|
|
||||||
function backToBoard(){
|
|
||||||
emit("showBoard");
|
|
||||||
}
|
|
||||||
|
|
||||||
const audio = ref<HTMLAudioElement | null>(null);
|
|
||||||
function startAudio(){
|
|
||||||
if( audio.value ){
|
|
||||||
audio.value.play();
|
|
||||||
} else {
|
|
||||||
audio.value = new Audio(question.value.audio);
|
|
||||||
audio.value.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function stopAudio(){
|
|
||||||
if( audio.value ){
|
|
||||||
audio.value.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -70,7 +31,6 @@ function stopAudio(){
|
||||||
<div class="col h-100 mx-3 overflow-y-auto">
|
<div class="col h-100 mx-3 overflow-y-auto">
|
||||||
<div class="ratio ratio-16x9">
|
<div class="ratio ratio-16x9">
|
||||||
<div class="w-100 h-100 d-flex justify-content-center align-items-center">
|
<div class="w-100 h-100 d-flex justify-content-center align-items-center">
|
||||||
<template v-if="isQuestionShown">
|
|
||||||
<span v-if="boardEntry.questions.length === 0" class="fs-1">
|
<span v-if="boardEntry.questions.length === 0" class="fs-1">
|
||||||
No Question to show
|
No Question to show
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -92,65 +52,18 @@ function stopAudio(){
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="question.questionType.id === QUESTION_TYPE_AUDIO_ID">
|
|
||||||
<div class="d-flex flex-column justify-content-center align-items-center h-100 w-100">
|
|
||||||
<span class="text-center preserve-breaks" :style="`font-size: ${question.fontScaling}em`">
|
|
||||||
{{ question.text }}
|
|
||||||
</span>
|
|
||||||
<div class="h-75 w-100 d-flex justify-content-center align-items-center">
|
|
||||||
<img
|
|
||||||
v-if="question.audio"
|
|
||||||
src="/src/assets/images/SoundWave.png"
|
|
||||||
alt="User uploaded - No caption available"
|
|
||||||
class="h-100 w-100 object-contain"
|
|
||||||
>
|
|
||||||
<button class="btn btn-primary" @click="startAudio">
|
|
||||||
Play Audio
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" @click="stopAudio">
|
|
||||||
Stop Audio
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<!-- Category Name -->
|
<!-- Category Name -->
|
||||||
<div class="position-absolute top-0 start-0">
|
<div class="position-absolute top-0 start-0 mt-2">
|
||||||
<span class="fs-2">
|
<span class="fs-2">
|
||||||
{{ }}
|
{{ boardEntry.category.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Points -->
|
<!-- Points -->
|
||||||
<div class="position-absolute top-0 end-0">
|
<div class="position-absolute bottom-0 end-0">
|
||||||
<span class="fs-2">
|
<span class="fs-2">
|
||||||
{{ boardEntry.points }}
|
{{ boardEntry.points }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Back to Board -->
|
|
||||||
<div class="position-absolute bottom-0 end-0">
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-outline-primary" @click="backToBoard">
|
|
||||||
Back to Board
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Back to Board -->
|
|
||||||
<div class="position-absolute top-0 start-50 translate-middle-x">
|
|
||||||
<div class="mt-2">
|
|
||||||
<button v-if="props.isQuestionShown" class="btn btn-outline-primary me-3" @click="hideQuestion">
|
|
||||||
Hide Question
|
|
||||||
</button>
|
|
||||||
<button v-else class="btn btn-outline-primary me-3" @click="showQuestion">
|
|
||||||
Show Question
|
|
||||||
</button>
|
|
||||||
<button v-if="props.isAnswerShown" class="btn btn-outline-primary" @click="hideAnswer">
|
|
||||||
Hide Answer
|
|
||||||
</button>
|
|
||||||
<button v-else class="btn btn-outline-primary" @click="showAnswer">
|
|
||||||
Show Answer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Answer -->
|
<!-- Answer -->
|
||||||
<div v-if="isAnswerShown" class="position-absolute bottom-0 start-50 translate-middle-x mb-2">
|
<div v-if="isAnswerShown" class="position-absolute bottom-0 start-50 translate-middle-x mb-2">
|
||||||
<div class="bg-primary p-2 rounded bg-opacity-50 fs-4 text-center">
|
<div class="bg-primary p-2 rounded bg-opacity-50 fs-4 text-center">
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,32 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type { Board } from '@/models/board/Board';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { userService } from '@/services/UserService';
|
|
||||||
import IconJeobeardy from '../icons/IconJeobeardy.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
withNewOption: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const boards = ref<Array<Board>>([]);
|
const boards = ref([{
|
||||||
|
id: 1,
|
||||||
|
boardName: "mockBoard",
|
||||||
|
},{
|
||||||
|
id: 2,
|
||||||
|
boardName: "test Mock",
|
||||||
|
},{
|
||||||
|
id: 3,
|
||||||
|
boardName: "Mocka Board",
|
||||||
|
},{
|
||||||
|
id: 4,
|
||||||
|
boardName: "Mocka Board 2",
|
||||||
|
}]);
|
||||||
|
|
||||||
function editBoard(board: Board){
|
function createNewBoard(){
|
||||||
router.push({
|
router.push( { name: 'create' } );
|
||||||
name: 'create',
|
|
||||||
params: {
|
|
||||||
boardId: board.id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewBoard() {
|
|
||||||
if (props.withNewOption) {
|
|
||||||
router.push({ name: 'create' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const boardsLoading = ref(true);
|
|
||||||
userService
|
|
||||||
.getBoardsForUser()
|
|
||||||
.then((loadedBoards) => {
|
|
||||||
boards.value = loadedBoards;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
boardsLoading.value = false;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<template v-if="boardsLoading">
|
|
||||||
<div class="col-12 d-flex justify-content-center py-3">
|
|
||||||
<IconJeobeardy class="spinny-spin spinny-size" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<template v-for="board in boards" :key="board.id">
|
<template v-for="board in boards" :key="board.id">
|
||||||
<div class="col-4 mb-3">
|
<div class="col-4 mb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -60,7 +37,7 @@ userService
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex justify-content-around align-items-center">
|
<div class="d-flex justify-content-around align-items-center">
|
||||||
<button class="btn btn-sm btn-primary" @click="editBoard(board)">
|
<button class="btn btn-sm btn-primary">
|
||||||
<FontAwesomeIcon :icon="['fas', 'edit']" />
|
<FontAwesomeIcon :icon="['fas', 'edit']" />
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -75,22 +52,11 @@ userService
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="props.withNewOption" class="col-4 mb-3">
|
<div class="col-4 mb-3">
|
||||||
<button
|
<button class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center" @click="createNewBoard">
|
||||||
class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center align-items-center gap-2"
|
|
||||||
@click="createNewBoard"
|
|
||||||
>
|
|
||||||
Create new Board
|
Create new Board
|
||||||
<FontAwesomeIcon :icon="['fas', 'plus']" size="2x" />
|
<FontAwesomeIcon :icon="['fas', 'plus']" size="2x"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.spinny-size{
|
|
||||||
max-width: 6.66em;
|
|
||||||
max-height: 6.66em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -29,7 +29,7 @@ function boardEntrySelected(cIndex: number, bEIndex: number){
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center p-3">
|
<div class="col text-center p-3">
|
||||||
<h2>
|
<h2>
|
||||||
{{ board.boardName }}
|
{{ board.name }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,12 +37,7 @@ function boardEntrySelected(cIndex: number, bEIndex: number){
|
||||||
<template v-for="(category, categoryIndex) in props.board.categories " :key="category.name">
|
<template v-for="(category, categoryIndex) in props.board.categories " :key="category.name">
|
||||||
<div class="col pb-2">
|
<div class="col pb-2">
|
||||||
<div class="d-flex flex-column h-100">
|
<div class="d-flex flex-column h-100">
|
||||||
<button
|
<button class="flex-fill board-card-max-height card bg-primary w-100 my-1" @click="categorySelected(categoryIndex)" :title="board.categories[categoryIndex].description">
|
||||||
class="flex-fill board-card-max-height card bg-primary w-100 my-1"
|
|
||||||
@click="categorySelected(categoryIndex)"
|
|
||||||
:title="board.categories[categoryIndex].description"
|
|
||||||
:style="[board.categories[categoryIndex].customColor ? {'background-color': `${board.categories[categoryIndex].color} !important`} : {}]"
|
|
||||||
>
|
|
||||||
<div class="card-body d-flex align-items-center justify-content-center">
|
<div class="card-body d-flex align-items-center justify-content-center">
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ function editQuestion(cIndex: number, bEIndex: number, qIndex: number){
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="d-flex flex-column h-100">
|
||||||
|
<div class="flex-grow-1 overflow-y-auto p-2">
|
||||||
<EditBoardPanel
|
<EditBoardPanel
|
||||||
v-if="props.categoryIndex === null && props.boardEntryIndex === null && props.questionIndex === null"
|
v-if="props.categoryIndex === null && props.boardEntryIndex === null && props.questionIndex === null"
|
||||||
v-model="board"
|
v-model="board"
|
||||||
|
|
@ -68,6 +69,21 @@ function editQuestion(cIndex: number, bEIndex: number, qIndex: number){
|
||||||
@editBoard="editBoard"
|
@editBoard="editBoard"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border-top border-2 border-primary p-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1 me-1">
|
||||||
|
<button class="btn btn-primary w-100">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ms-1">
|
||||||
|
<button class="btn btn-danger w-100">
|
||||||
|
Exit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { VueDraggable } from 'vue-draggable-plus';
|
import { VueDraggable } from 'vue-draggable-plus';
|
||||||
import type { Board } from '@/models/board/Board';
|
import type { Board } from '@/models/board/Board';
|
||||||
import { Question } from '@/models/board/Question';
|
import { Question } from '@/models/board/Question';
|
||||||
import { QuestionType, type QuestionTypeType } from '@/models/board/QuestionType';
|
import { QuestionType } from '@/models/board/QuestionType';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { questionTypesKey } from '@/services/UtilService';
|
||||||
import { AnswerType } from '@/models/board/AnswerType';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ const props = defineProps<{
|
||||||
categoryIndex: number,
|
categoryIndex: number,
|
||||||
boardEntryIndex: number,
|
boardEntryIndex: number,
|
||||||
}>();
|
}>();
|
||||||
|
const questionTypes = inject(questionTypesKey);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
editBoard: [],
|
editBoard: [],
|
||||||
|
|
@ -28,12 +29,12 @@ const boardEntry = computed( () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const newQuestionText = ref( '' );
|
const newQuestionText = ref( '' );
|
||||||
const newQuestionType = ref<QuestionTypeType>(QuestionType.TEXT);
|
const newQuestionType = ref<QuestionType | null>( (questionTypes ?? [null])[0] );
|
||||||
function addQuestion() {
|
function addQuestion() {
|
||||||
if( boardEntry.value.questions.length >= 10) {
|
if( boardEntry.value.questions.length >= 10 || newQuestionType.value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newQuestion = new Question(newQuestionText.value, newQuestionType.value);
|
const newQuestion = new Question(newQuestionText.value, newQuestionType.value, boardEntry.value );
|
||||||
boardEntry.value.questions.push( newQuestion );
|
boardEntry.value.questions.push( newQuestion );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +84,7 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
|
||||||
<ol class="breadcrumb mb-0 flex-nowrap">
|
<ol class="breadcrumb mb-0 flex-nowrap">
|
||||||
<li class="breadcrumb-item text-truncate">
|
<li class="breadcrumb-item text-truncate">
|
||||||
<a href="#" @click="openBoard">
|
<a href="#" @click="openBoard">
|
||||||
{{ board.boardName }}
|
{{ board.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item text-truncate">
|
<li class="breadcrumb-item text-truncate">
|
||||||
|
|
@ -107,7 +108,7 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
|
||||||
<label for="category-name">{{ t( 'board.boardentry.name' ) }}</label>
|
<label for="category-name">{{ t( 'board.boardentry.name' ) }}</label>
|
||||||
<input type="text" id="category-name" class="form-control mb-2" v-model="boardEntry.name" :placeholder="t( 'board.boardentry.name' )">
|
<input type="text" id="category-name" class="form-control mb-2" v-model="boardEntry.name" :placeholder="t( 'board.boardentry.name' )">
|
||||||
<label for="board-entry-points">{{ t( 'board.boardentry.points' ) }}</label>
|
<label for="board-entry-points">{{ t( 'board.boardentry.points' ) }}</label>
|
||||||
<input type="number" id="board-entry-points" class="form-control" v-model="boardEntry.points" :placeholder="t( 'board.boardentry.points' )">
|
<input type="text" id="board-entry-points" class="form-control" v-model="boardEntry.points" :placeholder="t( 'board.boardentry.points' )">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
|
|
@ -131,7 +132,7 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
|
||||||
</div>
|
</div>
|
||||||
<div class="text-truncate">
|
<div class="text-truncate">
|
||||||
<span class="ms-2">
|
<span class="ms-2">
|
||||||
<FontAwesomeIcon v-if="question.questionType === QuestionType.TEXT" :icon="['fas', 'align-center']" />
|
{{ question.questionType.title }}
|
||||||
<span class="fw-light">
|
<span class="fw-light">
|
||||||
({{ question.text.length === 0 ? 'No Text yet' : question.text }})
|
({{ question.text.length === 0 ? 'No Text yet' : question.text }})
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -164,12 +165,12 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
|
||||||
</template>
|
</template>
|
||||||
</VueDraggable>
|
</VueDraggable>
|
||||||
</template>
|
</template>
|
||||||
<label class="mt-2" for="type-for-new-question">{{ t( "board.question.add" ) }}</label>
|
<label class="mt-2" for="new-category-name">{{ t( "board.question.add" ) }}</label>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select id="type-for-new-question" v-model="newQuestionType" class="form-select">
|
<select id="type-for-new-question" v-model="newQuestionType" class="form-select">
|
||||||
<template v-for="questionType in QuestionType" :key="questionType">
|
<template v-for="questionType in questionTypes" :key="questionType.id">
|
||||||
<option :value="questionType" :title="t(`board.question.types.description.${questionType}`)">{{ t(`board.question.types.title.${questionType}`) }}</option>
|
<option :value="questionType" :title="questionType.description">{{ questionType.title }}</option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -182,13 +183,7 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4>{{ t('board.answer.label', 2) }}</h4>
|
<h4>{{ t('board.answer.label', 2) }}</h4>
|
||||||
<label class="mt-2" for="type-for-answer">{{ t( "board.answer.types.label" ) }}</label>
|
<label for="answer-text">{{ t( 'board.answer.text' ) }}</label>
|
||||||
<select id="type-for-answer" v-model="boardEntry.answer.answerType" class="form-select">
|
|
||||||
<template v-for="answerType in AnswerType" :key="answerType">
|
|
||||||
<option :value="answerType" :title="t(`board.answer.types.description.${answerType}`)">{{ t(`board.answer.types.title.${answerType}`) }}</option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
<label class="mt-2" for="answer-text">{{ t( 'board.answer.text' ) }}</label>
|
|
||||||
<textarea id="answer-text" v-model="boardEntry.answer.text" class="form-control mb-2"
|
<textarea id="answer-text" v-model="boardEntry.answer.text" class="form-control mb-2"
|
||||||
:placeholder="t( 'board.answer.text' )"></textarea>
|
:placeholder="t( 'board.answer.text' )"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Category } from '@/models/board/Category';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { VueDraggable } from 'vue-draggable-plus';
|
import { VueDraggable } from 'vue-draggable-plus';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ function addCategory() {
|
||||||
if( board.value.categories.length >= 12 ) {
|
if( board.value.categories.length >= 12 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newCategory = new Category( newCategoryName.value, '' );
|
const newCategory = new Category( newCategoryName.value, '', board.value );
|
||||||
board.value.categories.push( newCategory );
|
board.value.categories.push( newCategory );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +57,7 @@ function openCategory( index: number ) {
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2 class="border-bottom border-3 border-primary">{{ t( "board.infos" ) }}</h2>
|
<h2 class="border-bottom border-3 border-primary">{{ t( "board.infos" ) }}</h2>
|
||||||
<label for="board-name">{{ t( 'board.name' ) }}</label>
|
<label for="board-name">{{ t( 'board.name' ) }}</label>
|
||||||
<input type="text" id="board-name" class="form-control" v-model="board.boardName" :placeholder="t( 'board.name' )">
|
<input type="text" id="board-name" class="form-control" v-model="board.name" :placeholder="t( 'board.name' )">
|
||||||
<div class="form-check mt-2">
|
<div class="form-check mt-2">
|
||||||
<input class="form-check-input" type="checkbox" id="points-for-title" v-model="board.pointsAreTitle">
|
<input class="form-check-input" type="checkbox" id="points-for-title" v-model="board.pointsAreTitle">
|
||||||
<label for="points-for-title" class="form-check-label">{{ t( 'board.options.pointsForTitle' ) }}</label>
|
<label for="points-for-title" class="form-check-label">{{ t( 'board.options.pointsForTitle' ) }}</label>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import type { Board } from '@/models/board/Board';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { VueDraggable } from 'vue-draggable-plus';
|
import { VueDraggable } from 'vue-draggable-plus';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { BoardEntry } from '@/models/board/BoardEntry';
|
import { BoardEntry } from '@/models/board/BoardEntry';
|
||||||
import { Answer } from '@/models/board/Answer';
|
import { Answer } from '@/models/board/Answer';
|
||||||
import { AnswerType } from '@/models/board/AnswerType';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -26,8 +26,8 @@ function addBoardEntry() {
|
||||||
if( board.value.categories[props.categoryIndex].boardEntries.length >= 10 ) {
|
if( board.value.categories[props.categoryIndex].boardEntries.length >= 10 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const answer = new Answer('', AnswerType.TEXT);
|
const answer = new Answer('', undefined);
|
||||||
const newBoardEntry = new BoardEntry( newBoardEntryName.value, answer, [] );
|
const newBoardEntry = new BoardEntry( newBoardEntryName.value, 0, board.value.categories[props.categoryIndex], answer, [] );
|
||||||
board.value.categories[props.categoryIndex].boardEntries.push( newBoardEntry );
|
board.value.categories[props.categoryIndex].boardEntries.push( newBoardEntry );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,28 +66,22 @@ function openBoard() {
|
||||||
<div class="flex-grow-1 p-2">
|
<div class="flex-grow-1 p-2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex border-bottom border-3 border-primary align-items-center">
|
<div class="d-flex border-bottom border-3 border-primary align-items-center mb-2">
|
||||||
<button class="btn btn-sm btn-outline-primary mb-1" :title="t('board.category.back')" @click="openBoard">
|
<button class="btn btn-sm btn-outline-primary mb-1" :title="t('board.category.back')" @click="openBoard">
|
||||||
<FontAwesomeIcon :icon="['fas', 'angle-left']"/>
|
<FontAwesomeIcon :icon="['fas', 'angle-left']"/>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="flex-grow-1 ms-2">{{ t( "board.category.infos" ) }}</h2>
|
<h2 class="flex-grow-1 ms-2">{{ t( "board.category.infos" ) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="border-bottom border-2 border-primary">General Infos</h4>
|
|
||||||
<label for="category-name">{{ t( 'board.category.name' ) }}</label>
|
<label for="category-name">{{ t( 'board.category.name' ) }}</label>
|
||||||
<input type="text" id="category-name" class="form-control mb-2" v-model="board.categories[props.categoryIndex].name" :placeholder="t( 'board.category.name' )">
|
<input type="text" id="category-name" class="form-control mb-2" v-model="board.categories[props.categoryIndex].name" :placeholder="t( 'board.category.name' )">
|
||||||
<label for="category-description">{{ t( 'board.category.description' ) }}</label>
|
<label for="category-description">{{ t( 'board.category.description' ) }}</label>
|
||||||
<textarea id="category-description" class="form-control" v-model="board.categories[props.categoryIndex].description" :placeholder="t( 'board.category.description' )">
|
<textarea id="category-description" class="form-control" v-model="board.categories[props.categoryIndex].description" :placeholder="t( 'board.category.description' )">
|
||||||
</textarea>
|
</textarea>
|
||||||
<div class="form-check mt-2">
|
|
||||||
<input id="category-color-custom" type="checkbox" class="form-check-input" v-model="board.categories[props.categoryIndex].customColor">
|
|
||||||
<label for="category-color-custom">{{ t( 'board.category.custom-color' ) }}</label>
|
|
||||||
</div>
|
|
||||||
<input v-if="board.categories[props.categoryIndex].customColor" id="category-color" type="color" class="form-control" v-model="board.categories[props.categoryIndex].color"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="border-bottom border-2 border-primary">{{ t( "board.boardentry.label", 2 ) }}</h4>
|
<h4>{{ t( "board.boardentry.label", 2 ) }}</h4>
|
||||||
<template v-if=" board.categories[props.categoryIndex].boardEntries.length === 0 ">
|
<template v-if=" board.categories[props.categoryIndex].boardEntries.length === 0 ">
|
||||||
<p>
|
<p>
|
||||||
{{ t( "board.boardentry.label", 0 ) }}
|
{{ t( "board.boardentry.label", 0 ) }}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Board } from '@/models/board/Board';
|
import type { Board } from '@/models/board/Board';
|
||||||
import { computed } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { QuestionType } from '@/models/board/QuestionType';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
import { questionTypesKey } from '@/services/UtilService';
|
||||||
|
|
||||||
|
const QUESTION_TYPE_IMAGE_ID = 2;
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -13,6 +16,7 @@ const props = defineProps<{
|
||||||
boardEntryIndex: number,
|
boardEntryIndex: number,
|
||||||
questionIndex: number;
|
questionIndex: number;
|
||||||
}>();
|
}>();
|
||||||
|
const questionTypes = inject( questionTypesKey );
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
editBoard: [],
|
editBoard: [],
|
||||||
|
|
@ -34,21 +38,16 @@ function openBoardEntry( categoryIndex: number, boardEntryIndex: number ) {
|
||||||
emit( "editBoardEntry", categoryIndex, boardEntryIndex );
|
emit( "editBoardEntry", categoryIndex, boardEntryIndex );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageInput = ref<File | null>( null );
|
||||||
function newImageUploaded( event: Event ) {
|
function newImageUploaded( event: Event ) {
|
||||||
const element = event.currentTarget as HTMLInputElement;
|
const element = event.currentTarget as HTMLInputElement;
|
||||||
let files = element.files;
|
let files = element.files;
|
||||||
if( files === null || files.length === 0 ) {
|
if( files === null || files.length === 0 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
imageInput.value = files[0];
|
||||||
question.value.image = URL.createObjectURL(files[0]);
|
question.value.image = URL.createObjectURL(files[0]);
|
||||||
}
|
|
||||||
function newAudioUploaded( event: Event ) {
|
|
||||||
const element = event.currentTarget as HTMLInputElement;
|
|
||||||
let files = element.files;
|
|
||||||
if( files === null || files.length === 0 ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
question.value.audio = URL.createObjectURL(files[0]);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -66,7 +65,7 @@ function newAudioUploaded( event: Event ) {
|
||||||
<ol class="breadcrumb mb-0 flex-nowrap">
|
<ol class="breadcrumb mb-0 flex-nowrap">
|
||||||
<li class="breadcrumb-item text-truncate">
|
<li class="breadcrumb-item text-truncate">
|
||||||
<a href="#" @click="openBoard">
|
<a href="#" @click="openBoard">
|
||||||
{{ board.boardName }}
|
{{ board.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item text-truncate">
|
<li class="breadcrumb-item text-truncate">
|
||||||
|
|
@ -80,7 +79,7 @@ function newAudioUploaded( event: Event ) {
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active text-truncate" style="max-width: 6em;" aria-current="page">
|
<li class="breadcrumb-item active text-truncate" style="max-width: 6em;" aria-current="page">
|
||||||
{{ question.text.length !== 0 ? question.text : t(`board.question.types.title.${question.questionType}`) }}
|
{{ question.text.length !== 0 ? question.text : question.questionType.title }}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -102,12 +101,12 @@ function newAudioUploaded( event: Event ) {
|
||||||
|
|
||||||
<label for="board-entry-points">{{ t( 'board.question.type' ) }}</label>
|
<label for="board-entry-points">{{ t( 'board.question.type' ) }}</label>
|
||||||
<select id="question-type" v-model="question.questionType" class="form-select mb-2" aria-label="Question Type">
|
<select id="question-type" v-model="question.questionType" class="form-select mb-2" aria-label="Question Type">
|
||||||
<template v-for=" questionType in QuestionType " :key="questionType">
|
<template v-for=" questionType in questionTypes " :key="questionType.id">
|
||||||
<option :value="questionType" :title="t(`board.question.types.description.${questionType}`)">{{ t(`board.question.types.title.${questionType}`) }}</option>
|
<option :value="questionType" :title="questionType.description">{{ questionType.title }}</option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<template v-if=" question.questionType === QuestionType.IMAGE ">
|
<template v-if=" question.questionType.id === QUESTION_TYPE_IMAGE_ID ">
|
||||||
<label for="question-image-input">{{ t( 'board.question.upload.image' ) }}</label>
|
<label for="question-image-input">{{ t( 'board.question.upload.image' ) }}</label>
|
||||||
<input
|
<input
|
||||||
id="question-image-input"
|
id="question-image-input"
|
||||||
|
|
@ -117,16 +116,6 @@ function newAudioUploaded( event: Event ) {
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-if=" question.questionType === QuestionType.AUDIO ">
|
|
||||||
<label for="question-audio-input">{{ t( 'board.question.upload.audio' ) }}</label>
|
|
||||||
<input
|
|
||||||
id="question-audio-input"
|
|
||||||
type="file"
|
|
||||||
class="form-control mb-2"
|
|
||||||
@change="newAudioUploaded"
|
|
||||||
accept="audio/mpeg"
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="row g-2 w-100">
|
|
||||||
<div class="col d-flex justify-content-around">
|
|
||||||
<button class="btn btn-primary">
|
|
||||||
{{ t('game.host.button.letNextChoose') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
lockQuestion: [delay: number],
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
|
|
||||||
const delayBeforeLock = ref(0);
|
|
||||||
function lockQuestion(){
|
|
||||||
emit("lockQuestion", delayBeforeLock.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="row g-2 w-100">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-5">
|
|
||||||
<button class="btn btn-primary w-100" @click="lockQuestion">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'lock']" />
|
|
||||||
{{ t('game.host.button.lockIn') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-7 d-flex align-items-center">
|
|
||||||
<p class="mb-0 mx-1">
|
|
||||||
{{ t('game.host.text.in') }}
|
|
||||||
</p>
|
|
||||||
<input class="form-control" type="number" id="time-before-lock" v-model="delayBeforeLock">
|
|
||||||
<p class="mb-0 ms-1">
|
|
||||||
{{ t('game.host.text.seconds') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<button class="btn btn-primary w-100">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'eye']" />
|
|
||||||
{{ t('game.host.button.revealAll') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { onMounted, watch } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import ThemeChanger from '@/components/blocks/ThemeChanger.vue';
|
||||||
import LocaleChanger from '@/components/blocks/LocaleChanger.vue';
|
import LocaleChanger from '@/components/blocks/LocaleChanger.vue';
|
||||||
import { useUserStore } from '@/stores/UserStore';
|
import { useUserStore } from '@/stores/UserStore';
|
||||||
import { authService } from '@/services/AuthService';
|
import { authService } from '@/services/AuthService';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
const navNames = {
|
const navNames = {
|
||||||
HOME: "home",
|
HOME: "home",
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useGameStore } from '@/stores/GameStore';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const gameStore = useGameStore();
|
|
||||||
|
|
||||||
function adjustPoints(){
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h3 class="border-bottom border-primary border-2">Players</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<label for="points-manuel-adjustment-value">{{
|
|
||||||
t('game.pointsAdjustment')
|
|
||||||
}}</label>
|
|
||||||
<input
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
type="number"
|
|
||||||
id="points-manuel-adjustment-value"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-for="player in gameStore.players" :key="player.uuid">
|
|
||||||
<div class="card bg-body-secondary mb-2">
|
|
||||||
<div class="card-header bg-primary p-2">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col d-flex justify-content-between">
|
|
||||||
<span class="text-break me-3">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
v-if="player.isAnswering"
|
|
||||||
:icon="['fas', 'angles-right']"
|
|
||||||
class="text-dark"
|
|
||||||
beat-fade
|
|
||||||
/>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
v-if="player.isChoosing"
|
|
||||||
:icon="['fas', 'hand-pointer']"
|
|
||||||
class="text-dark"
|
|
||||||
/>
|
|
||||||
{{ player.name }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ player.points }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3">Answer</div>
|
|
||||||
<div class="col-9">
|
|
||||||
<input
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
type="text"
|
|
||||||
id="points-player-nr"
|
|
||||||
value=""
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="gameStore.isHost" class="row mt-2">
|
|
||||||
<div class="col">
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-6">
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100" @click="adjustPoints()">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'plus']" />
|
|
||||||
{{ t('game.host.button.pointsAdd') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100" @click="adjustPoints()">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'minus']" />
|
|
||||||
{{ t('game.host.button.pointsSubtract') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100 me-2">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'eye']" />
|
|
||||||
{{ t('game.host.button.reveal') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'hand-pointer']" />
|
|
||||||
{{ t('game.host.button.letChoose') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'lock-open']" />
|
|
||||||
{{ t('game.host.button.unlock') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<template v-if="false /* isQuestion Locked */">
|
|
||||||
<div class="col-6">
|
|
||||||
<button class="btn btn-sm btn-success w-100">
|
|
||||||
{{ t('game.host.button.correct') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<button class="btn btn-sm btn-danger w-100">
|
|
||||||
{{ t('game.host.button.wrong') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,44 +1,52 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, type Ref } from 'vue';
|
import { computed, inject, provide, ref, type Ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { navbarKey } from '@/services/UtilService';
|
import { navbarKey, questionTypesKey } from '@/services/UtilService';
|
||||||
import { Board } from '@/models/board/Board';
|
import { Board } from '@/models/board/Board';
|
||||||
import { BoardEntry } from '@/models/board/BoardEntry';
|
|
||||||
|
|
||||||
import type NavBar from '@/components/blocks/NavBar.vue';
|
import type NavBar from '@/components/blocks/NavBar.vue';
|
||||||
|
|
||||||
import BoardView from '@/components/blocks/BoardView.vue';
|
import BoardView from '@/components/blocks/BoardView.vue';
|
||||||
import CreatePanel from '@/components/blocks/CreatePanel.vue';
|
import CreatePanel from '@/components/blocks/CreatePanel.vue';
|
||||||
import BoardEntryView from '@/components/blocks/BoardEntryView.vue';
|
import { Category } from '@/models/board/Category';
|
||||||
import { userService } from '@/services/UserService';
|
import BoardEntryView from '../blocks/BoardEntryView.vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { BoardEntry } from '@/models/board/BoardEntry';
|
||||||
|
import { Answer } from '@/models/board/Answer';
|
||||||
|
import { QuestionType } from '@/models/board/QuestionType';
|
||||||
|
|
||||||
const navbar = inject<Ref<InstanceType<typeof NavBar> | null>>(navbarKey);
|
const { t } = useI18n();
|
||||||
const navbarHeight = computed(() => {
|
|
||||||
|
const navbar = inject<Ref<InstanceType<typeof NavBar> | null>>( navbarKey );
|
||||||
|
const navbarHeight = computed( () => {
|
||||||
return navbar?.value?.navElement?.clientHeight;
|
return navbar?.value?.navElement?.clientHeight;
|
||||||
});
|
} );
|
||||||
const restHeight = computed(() => {
|
const restHeight = computed( () => {
|
||||||
return { height: `calc(100vh - ${navbarHeight.value}px)` };
|
return { height: `calc(100vh - ${navbarHeight.value}px)` };
|
||||||
});
|
} );
|
||||||
|
|
||||||
const board = ref<Board>(new Board('New Board'));
|
const board = ref<Board>( new Board( "New Board" ) );
|
||||||
|
const board1 = ref<Board>( new Board( "New Board", [new Category( "Test1", "", board.value as Board ), new Category( "Test2", "", board.value as Board )] ) );
|
||||||
|
const answer = new Answer( '', undefined );
|
||||||
|
const newBoardEntry = new BoardEntry( "Test Entry 1", board1.value.categories[0] as Category, answer, [] );
|
||||||
|
board1.value.categories[0].boardEntries.push( newBoardEntry );
|
||||||
|
|
||||||
const categoryIndex = ref<number | null>(null);
|
const categoryIndex = ref<number | null>( null );
|
||||||
const boardEntryIndex = ref<number | null>(null);
|
const boardEntryIndex = ref<number | null>( null );
|
||||||
const questionIndex = ref<number | null>(null);
|
const questionIndex = ref<number | null>( null );
|
||||||
|
|
||||||
function showQuestionLayer(cIndex: number, bEIndex: number, qIndex: number) {
|
function showQuestion( cIndex: number, bEIndex: number, qIndex: number ) {
|
||||||
categoryIndex.value = cIndex;
|
categoryIndex.value = cIndex;
|
||||||
boardEntryIndex.value = bEIndex;
|
boardEntryIndex.value = bEIndex;
|
||||||
questionIndex.value = qIndex;
|
questionIndex.value = qIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showBoardEntry(cIndex: number, bEIndex: number) {
|
function showBoardEntry( cIndex: number, bEIndex: number ) {
|
||||||
categoryIndex.value = cIndex;
|
categoryIndex.value = cIndex;
|
||||||
boardEntryIndex.value = bEIndex;
|
boardEntryIndex.value = bEIndex;
|
||||||
questionIndex.value = null;
|
questionIndex.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCategory(cIndex: number) {
|
function showCategory( cIndex: number ) {
|
||||||
categoryIndex.value = cIndex;
|
categoryIndex.value = cIndex;
|
||||||
boardEntryIndex.value = null;
|
boardEntryIndex.value = null;
|
||||||
questionIndex.value = null;
|
questionIndex.value = null;
|
||||||
|
|
@ -50,139 +58,44 @@ function showBoard() {
|
||||||
questionIndex.value = null;
|
questionIndex.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isQuestionShown = ref(true);
|
const questionTypes = [
|
||||||
const isAnswerShown = ref(true);
|
new QuestionType("Simple Text", "A simple question with just text", true, 1),
|
||||||
function showQuestion() {
|
new QuestionType("Image Question", "A question with text and an image", true, 2),
|
||||||
isQuestionShown.value = true;
|
new QuestionType("Audio Question", "A question with text and some audio", true, 3),
|
||||||
}
|
];
|
||||||
function hideQuestion() {
|
|
||||||
isQuestionShown.value = false;
|
|
||||||
}
|
|
||||||
function showAnswer() {
|
|
||||||
isAnswerShown.value = true;
|
|
||||||
}
|
|
||||||
function hideAnswer() {
|
|
||||||
isAnswerShown.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const savingBoardInProgress = ref(false);
|
provide(questionTypesKey, questionTypes);
|
||||||
function saveBoard() {
|
|
||||||
savingBoardInProgress.value = true;
|
|
||||||
let savePromise;
|
|
||||||
if( board.value.id ){
|
|
||||||
savePromise = userService.updateBoard(board.value);
|
|
||||||
} else {
|
|
||||||
savePromise = userService.saveNewBoard(board.value);
|
|
||||||
}
|
|
||||||
savePromise
|
|
||||||
.then((savedBoard) => {
|
|
||||||
board.value = savedBoard;
|
|
||||||
if( !route.params.boardId ){
|
|
||||||
router.replace({
|
|
||||||
name: 'create',
|
|
||||||
params: {
|
|
||||||
boardId: savedBoard.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch( (error) => {
|
|
||||||
console.error(error);
|
|
||||||
})
|
|
||||||
.finally( () => {
|
|
||||||
savingBoardInProgress.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function exit() {
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
const boardLoading = ref(true);
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
if (route.params.boardId) {
|
|
||||||
userService
|
|
||||||
.getBoard(route.params.boardId as string)
|
|
||||||
.then((boardResponse) => {
|
|
||||||
board.value = boardResponse;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
boardLoading.value = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
boardLoading.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :style="restHeight">
|
<div :style="restHeight">
|
||||||
<div class="row h-100">
|
<div class="row h-100">
|
||||||
<div class="col-9 h-100 pe-0">
|
<div class="col-9 h-100 pe-0">
|
||||||
<template v-if="boardLoading">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'spinner']" size="xl" spin />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<BoardEntryView
|
<BoardEntryView
|
||||||
v-if="categoryIndex !== null && boardEntryIndex !== null"
|
v-if=" categoryIndex !== null && boardEntryIndex !== null "
|
||||||
:boardEntry="( board.categories[categoryIndex].boardEntries[boardEntryIndex] as BoardEntry )"
|
:boardEntry="( board1.categories[categoryIndex].boardEntries[boardEntryIndex] as BoardEntry )"
|
||||||
:categoryName="board.categories[categoryIndex].name"
|
|
||||||
:selectedQuestionIndex="questionIndex ?? 0"
|
:selectedQuestionIndex="questionIndex ?? 0"
|
||||||
@questionSelected="( qIndex ) => showQuestionLayer( categoryIndex!, boardEntryIndex!, qIndex )"
|
:isAnswerShown="true"
|
||||||
:isQuestionShown="isQuestionShown"
|
@questionSelected="(qIndex) => showQuestion(categoryIndex!, boardEntryIndex!, qIndex)"
|
||||||
:isAnswerShown="isAnswerShown"
|
|
||||||
@showQuestion="showQuestion"
|
|
||||||
@hideQuestion="hideQuestion"
|
|
||||||
@showAnswer="showAnswer"
|
|
||||||
@hideAnswer="hideAnswer"
|
|
||||||
@showBoard="showBoard"
|
|
||||||
/>
|
/>
|
||||||
<BoardView
|
<BoardView
|
||||||
v-else
|
v-else
|
||||||
:board="( board as Board )"
|
:board="( board1 as Board )"
|
||||||
@categorySelected="showCategory"
|
@categorySelected="showCategory"
|
||||||
@boardEntrySelected="showBoardEntry"
|
@boardEntrySelected="showBoardEntry" />
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="col-3 ps-0 h-100 overflow-auto border-start border-2 border-primary">
|
||||||
class="col-3 ps-0 h-100 overflow-auto border-start border-2 border-primary"
|
|
||||||
>
|
|
||||||
<div class="d-flex flex-column h-100">
|
|
||||||
<div class="flex-grow-1 overflow-y-auto p-2">
|
|
||||||
<CreatePanel
|
<CreatePanel
|
||||||
v-model="( board as Board )"
|
v-model="( board1 as Board )"
|
||||||
:categoryIndex="categoryIndex"
|
:categoryIndex="categoryIndex"
|
||||||
:boardEntryIndex="boardEntryIndex"
|
:boardEntryIndex="boardEntryIndex"
|
||||||
:questionIndex="questionIndex"
|
:questionIndex="questionIndex"
|
||||||
@editBoard="showBoard"
|
@editBoard="showBoard"
|
||||||
@editCategory="showCategory"
|
@editCategory="showCategory"
|
||||||
@editBoardEntry="showBoardEntry"
|
@editBoardEntry="showBoardEntry"
|
||||||
@editQuestion="showQuestionLayer"
|
@editQuestion="showQuestion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-top border-2 border-primary p-2">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1 me-1">
|
|
||||||
<button class="btn btn-primary w-100" @click="saveBoard">
|
|
||||||
<FontAwesomeIcon v-if="savingBoardInProgress" :icon="['fas', 'spinner']" spin />
|
|
||||||
<FontAwesomeIcon v-else :icon="['fas', 'save']" />
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow-1 ms-1">
|
|
||||||
<button class="btn btn-danger w-100" @click="exit">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'xmark']" />
|
|
||||||
Exit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,140 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, type Ref } from 'vue';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { navbarKey } from '@/services/UtilService';
|
const { t } = useI18n();
|
||||||
import { Board } from '@/models/board/Board';
|
|
||||||
import { Category } from '@/models/board/Category';
|
|
||||||
import { BoardEntry } from '@/models/board/BoardEntry';
|
|
||||||
import { Answer } from '@/models/board/Answer';
|
|
||||||
import { AnswerType } from '@/models/board/AnswerType';
|
|
||||||
|
|
||||||
import type NavBar from '@/components/blocks/NavBar.vue';
|
|
||||||
import BoardView from '@/components/blocks/BoardView.vue';
|
|
||||||
import BoardEntryView from '@/components/blocks/BoardEntryView.vue';
|
|
||||||
import PlayersPanel from '@/components/blocks/PlayersPanel.vue';
|
|
||||||
import { useGameStore } from '@/stores/GameStore';
|
|
||||||
import HostInteractionsBoard from '../blocks/HostInteractionsBoard.vue';
|
|
||||||
import HostInteractionsEntry from '../blocks/HostInteractionsEntry.vue';
|
|
||||||
|
|
||||||
const gameStore = useGameStore();
|
|
||||||
|
|
||||||
const navbar = inject<Ref<InstanceType<typeof NavBar> | null>>(navbarKey);
|
|
||||||
const navbarHeight = computed(() => {
|
|
||||||
return navbar?.value?.navElement?.clientHeight;
|
|
||||||
});
|
|
||||||
const restHeight = computed(() => {
|
|
||||||
return { height: `calc(100vh - ${navbarHeight.value}px)` };
|
|
||||||
});
|
|
||||||
|
|
||||||
const board = ref<Board>(new Board('New Board'));
|
|
||||||
const board1 = ref<Board>(
|
|
||||||
new Board('New Board', [
|
|
||||||
new Category('Test1', ''),
|
|
||||||
new Category('Test2', ''),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
const answer = new Answer(
|
|
||||||
'',
|
|
||||||
AnswerType.TEXT
|
|
||||||
);
|
|
||||||
const newBoardEntry = new BoardEntry(
|
|
||||||
'Test Entry 1',
|
|
||||||
answer,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
board1.value.categories[0].boardEntries.push(newBoardEntry);
|
|
||||||
|
|
||||||
const categoryIndex = ref<number | null>(null);
|
|
||||||
const boardEntryIndex = ref<number | null>(null);
|
|
||||||
const questionIndex = ref<number | null>(null);
|
|
||||||
|
|
||||||
function showQuestionLayer(cIndex: number, bEIndex: number, qIndex: number) {
|
|
||||||
categoryIndex.value = cIndex;
|
|
||||||
boardEntryIndex.value = bEIndex;
|
|
||||||
questionIndex.value = qIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showBoardEntry(cIndex: number, bEIndex: number) {
|
|
||||||
categoryIndex.value = cIndex;
|
|
||||||
boardEntryIndex.value = bEIndex;
|
|
||||||
questionIndex.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showBoard() {
|
|
||||||
categoryIndex.value = null;
|
|
||||||
boardEntryIndex.value = null;
|
|
||||||
questionIndex.value = null;
|
|
||||||
}
|
|
||||||
const isBoardShown = computed( () => {
|
|
||||||
return categoryIndex.value === null && boardEntryIndex.value === null && questionIndex.value === null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isQuestionShown = ref(true);
|
|
||||||
const isAnswerShown = ref(true);
|
|
||||||
function showQuestion() {
|
|
||||||
isQuestionShown.value = true;
|
|
||||||
}
|
|
||||||
function hideQuestion() {
|
|
||||||
isQuestionShown.value = false;
|
|
||||||
}
|
|
||||||
function showAnswer() {
|
|
||||||
isAnswerShown.value = true;
|
|
||||||
}
|
|
||||||
function hideAnswer() {
|
|
||||||
isAnswerShown.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :style="restHeight">
|
<div>
|
||||||
<div class="row h-100">
|
{{ t( 'about.whatis' ) }}
|
||||||
<div class="col-9 h-100 pe-0">
|
|
||||||
<BoardEntryView
|
|
||||||
v-if="categoryIndex !== null && boardEntryIndex !== null"
|
|
||||||
:boardEntry="( board1.categories[categoryIndex].boardEntries[boardEntryIndex] as BoardEntry )"
|
|
||||||
:categoryName="board1.categories[categoryIndex].name"
|
|
||||||
:selectedQuestionIndex="questionIndex ?? 0"
|
|
||||||
@questionSelected="( qIndex ) => showQuestionLayer( categoryIndex!, boardEntryIndex!, qIndex )"
|
|
||||||
:isQuestionShown="isQuestionShown"
|
|
||||||
:isAnswerShown="isAnswerShown"
|
|
||||||
@showQuestion="showQuestion"
|
|
||||||
@hideQuestion="hideQuestion"
|
|
||||||
@showAnswer="showAnswer"
|
|
||||||
@hideAnswer="hideAnswer"
|
|
||||||
@showBoard="showBoard"
|
|
||||||
/>
|
|
||||||
<BoardView
|
|
||||||
v-else
|
|
||||||
:board="( board1 as Board )"
|
|
||||||
@boardEntrySelected="showBoardEntry"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col-3 ps-0 h-100 overflow-auto border-start border-2 border-primary"
|
|
||||||
>
|
|
||||||
<div class="d-flex flex-column h-100">
|
|
||||||
<div class="flex-grow-1 overflow-y-auto p-2 overflow-x-hidden">
|
|
||||||
<PlayersPanel />
|
|
||||||
</div>
|
|
||||||
<div class="border-top border-2 border-primary p-2">
|
|
||||||
<template v-if="gameStore.isHost">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<template v-if="isBoardShown">
|
|
||||||
<HostInteractionsBoard />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<HostInteractionsEntry />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<PlayerActionPanel />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -3,11 +3,8 @@ import { ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { userService } from '@/services/UserService';
|
import { userService } from '@/services/UserService';
|
||||||
import { useGameStore } from '@/stores/GameStore';
|
|
||||||
|
|
||||||
import IconJeobeardy from '@/components/icons/IconJeobeardy.vue';
|
import IconJeobeardy from '@/components/icons/IconJeobeardy.vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -20,48 +17,6 @@ onMounted( () => {
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const inviteCode = ref('');
|
|
||||||
const joinLoading = ref(false);
|
|
||||||
const joinError = ref<null | string>(null);
|
|
||||||
function joinGame(){
|
|
||||||
joinLoading.value = true;
|
|
||||||
gameStore.initGame(inviteCode.value)
|
|
||||||
.then( (game) => {
|
|
||||||
router.push({name: 'join', params: { gameUuid: game.uuid } } )
|
|
||||||
})
|
|
||||||
.catch( (error: AxiosError) => {
|
|
||||||
joinError.value = error.message;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
joinLoading.value = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const hostLoading = ref(false);
|
|
||||||
const hostError = ref<null | string>(null);
|
|
||||||
const errorTimeout = ref<number | null>(null)
|
|
||||||
function startHost(){
|
|
||||||
hostLoading.value = true;
|
|
||||||
gameStore.startHost()
|
|
||||||
.then( (game) => {
|
|
||||||
router.push({name: 'lobby', params: { gameUuid: game.uuid } } )
|
|
||||||
})
|
|
||||||
.catch( (error: AxiosError) => {
|
|
||||||
hostError.value = error.message ?? null;
|
|
||||||
if( errorTimeout.value ){
|
|
||||||
clearTimeout(errorTimeout.value);
|
|
||||||
}
|
|
||||||
errorTimeout.value = setTimeout(() => {
|
|
||||||
hostError.value = null;
|
|
||||||
}, 5000);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
hostLoading.value = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -91,14 +46,10 @@ function startHost(){
|
||||||
</p>
|
</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<input type="text" class="form-control" placeholder="Code" v-model="inviteCode">
|
<input type="text" class="form-control" placeholder="Code">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button class="btn btn-primary" @click="joinGame">
|
<button class="btn btn-primary">{{ t("join.button") }}</button>
|
||||||
<FontAwesomeIcon v-if="joinLoading" :icon="['fas', 'spinner']" spin/>
|
|
||||||
<FontAwesomeIcon v-else :icon="['fas', 'arrow-right-to-bracket']" />
|
|
||||||
{{ t("join.button") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,21 +70,19 @@ function startHost(){
|
||||||
{{ t("host.text") }}
|
{{ t("host.text") }}
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
{{ t("host.alreadyCreatedBoard") }}
|
{{ t("host.alreadyHostedGome") }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ t("host.startHost") }}
|
{{ t("host.textCode") }}
|
||||||
</p>
|
</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button class="btn btn-primary" @click="startHost">
|
<input type="text" class="form-control" placeholder="Code">
|
||||||
<FontAwesomeIcon v-if="hostLoading" :icon="['fas', 'spinner']" spin/>
|
</div>
|
||||||
<FontAwesomeIcon v-else :icon="['fas', 'users']"/>
|
<div class="col-auto">
|
||||||
{{ t("host.button") }}
|
<button class="btn btn-primary">{{ t("host.button") }}</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-show="hostError !== null">{{ hostError }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { GAME_STATUS_CONST } from '@/services/GameService';
|
|
||||||
import { useGameStore } from '@/stores/GameStore';
|
|
||||||
import { useUserStore } from '@/stores/UserStore';
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const gameStore = useGameStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const playerName = ref('');
|
|
||||||
const inviteCode = ref('');
|
|
||||||
|
|
||||||
function backOrHome(){
|
|
||||||
if( window.history.length - userStore.initialHistoryLen > 0 ){
|
|
||||||
router.back();
|
|
||||||
} else {
|
|
||||||
router.push({name: 'home'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadGame(){
|
|
||||||
gameStore
|
|
||||||
.initGame(inviteCode.value)
|
|
||||||
.catch((error: AxiosError) => {
|
|
||||||
joinError.value = error.message;
|
|
||||||
console.error(error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
joinLoading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinGame(){
|
|
||||||
joinLoading.value = true;
|
|
||||||
gameStore.joinGame(playerName.value)
|
|
||||||
.then( () => {
|
|
||||||
if( gameStore.gameStatus === GAME_STATUS_CONST.lobby ) {
|
|
||||||
router.push({name: 'lobby', params: { gameUuid: gameId}})
|
|
||||||
}
|
|
||||||
if( gameStore.gameStatus === GAME_STATUS_CONST.inProgress ) {
|
|
||||||
router.push({name: 'game', params: { gameUuid: gameId}})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch( (error) => {
|
|
||||||
console.error(error);
|
|
||||||
})
|
|
||||||
.finally( () => {
|
|
||||||
joinLoading.value = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinError = ref<string | null>(null);
|
|
||||||
const joinLoading = ref(false);
|
|
||||||
const showingInviteCodeInput = ref(false);
|
|
||||||
const gameId = route.params.gameUuid as string | undefined;
|
|
||||||
|
|
||||||
const siteLoading = ref(true);
|
|
||||||
function initGame(gameId: string){
|
|
||||||
gameStore
|
|
||||||
.initGame(gameId)
|
|
||||||
.catch((error: AxiosError) => {
|
|
||||||
joinError.value = error.message;
|
|
||||||
showingInviteCodeInput.value = true;
|
|
||||||
})
|
|
||||||
.finally( () => {
|
|
||||||
siteLoading.value = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkIfGameSavedInSessionAndIsSameAsRoute(){
|
|
||||||
const sessionGame = gameStore.checkForGame();
|
|
||||||
if( sessionGame === null ){
|
|
||||||
if( gameId === undefined ){
|
|
||||||
showingInviteCodeInput.value = true;
|
|
||||||
siteLoading.value = false;
|
|
||||||
} else {
|
|
||||||
initGame(gameId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if( gameId === undefined ){
|
|
||||||
showingInviteCodeInput.value = true;
|
|
||||||
inviteCode.value = sessionGame.inviteCode;
|
|
||||||
siteLoading.value = false;
|
|
||||||
} else {
|
|
||||||
if( gameId === sessionGame.uuid && !gameStore.isConnected ){
|
|
||||||
initGame(gameId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if( gameId === sessionGame.inviteCode && !gameStore.isConnected ){
|
|
||||||
initGame(gameId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
siteLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfGameSavedInSessionAndIsSameAsRoute();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="d-flex flex-column w-100 justify-content-start align-items-center"
|
|
||||||
>
|
|
||||||
<div class="row mt-5">
|
|
||||||
<div class="col">
|
|
||||||
<template v-if="showingInviteCodeInput">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4>Join Game</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<label for="invite-code-input">{{ t("join.textCode") }}</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
id="invite-code-input"
|
|
||||||
v-model="inviteCode"
|
|
||||||
/>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
|
||||||
<button class="btn btn-outline-danger" @click="backOrHome">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'xmark']" />
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" @click="loadGame">
|
|
||||||
Join
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'arrow-right-to-bracket']" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-danger">{{ joinError }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4>Join Game</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul>
|
|
||||||
<li>Host: {{ gameStore.hostUsername }}</li>
|
|
||||||
<li>Players: {{ gameStore.playerCount }}</li>
|
|
||||||
<li>Status: {{ t(gameStore.gameStatus) }}</li>
|
|
||||||
</ul>
|
|
||||||
<label for="player-name-input">{{ t('game.player.name') }}</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
id="player-name-input"
|
|
||||||
v-model="playerName"
|
|
||||||
/>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
|
||||||
<button class="btn btn-outline-danger" @click="backOrHome">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'xmark']" />
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" @click="joinGame">
|
|
||||||
Join
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'arrow-right-to-bracket']" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-danger">{{ joinError }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useClipboard } from '@vueuse/core';
|
|
||||||
|
|
||||||
import { userService } from '@/services/UserService';
|
|
||||||
import { useGameStore } from '@/stores/GameStore';
|
|
||||||
import { Board } from '@/models/board/Board';
|
|
||||||
|
|
||||||
import IconJeobeardy from '@/components/icons/IconJeobeardy.vue';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const gameStore = useGameStore();
|
|
||||||
|
|
||||||
const boardSearchText = ref('');
|
|
||||||
|
|
||||||
const boardsLoading = ref(true);
|
|
||||||
const hostBoards = ref<Array<Board>>([])
|
|
||||||
if( gameStore.isHost ){
|
|
||||||
userService.getBoardsForUser()
|
|
||||||
.then( (boards) => {
|
|
||||||
hostBoards.value = boards;
|
|
||||||
} )
|
|
||||||
.catch( (error) => {
|
|
||||||
|
|
||||||
})
|
|
||||||
.finally( () => {
|
|
||||||
boardsLoading.value = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function startGame(){
|
|
||||||
gameStore.startGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { copy, copied } = useClipboard();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="d-flex flex-column w-100 justify-content-start align-items-center"
|
|
||||||
>
|
|
||||||
<div class="row w-100 mt-4 mb-3">
|
|
||||||
<div :class="[{'col-6': gameStore.isHost},{'col-12': !gameStore.isHost}]" class="d-flex justify-content-center align-items-center gap-3">
|
|
||||||
<h4>Invite Code:</h4>
|
|
||||||
<div class="input-group w-invite-code">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
aria-label="Invite code"
|
|
||||||
readonly
|
|
||||||
:value="gameStore.gameInviteCode"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-primary"
|
|
||||||
type="button"
|
|
||||||
id="copy-invite-code-to-clipboard"
|
|
||||||
@click="copy(gameStore.gameInviteCode ?? '')"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'copy']" />
|
|
||||||
<span v-if="copied" class="badge pe-0">
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'check']" />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="gameStore.isHost" class="col-6 d-flex justify-content-center align-items-center">
|
|
||||||
<button class="btn btn-primary" @click="startGame">
|
|
||||||
Start Game
|
|
||||||
<FontAwesomeIcon :icon="['fas', 'arrow-right']" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="gameStore.isHost">
|
|
||||||
<div class="row w-100 my-2 p-2 bg-body-secondary">
|
|
||||||
<div class="col-12 col-lg-9 text-start">
|
|
||||||
<h4>Boards</h4>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-3">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="search"
|
|
||||||
id="search-for-board"
|
|
||||||
v-model="boardSearchText"
|
|
||||||
placeholder="Search for a board"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row w-100 mb-3 pb-3 bg-body-secondary g-3 text-center">
|
|
||||||
<template v-if="boardsLoading">
|
|
||||||
<IconJeobeardy class="spinny-spin spinny-size" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<template v-for="board in hostBoards" :key="board.uuid">
|
|
||||||
<div class="col-12 col-lg-4">
|
|
||||||
<div class="card p-2">
|
|
||||||
{{ board.boardName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h3>Players</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row w-100 px-3">
|
|
||||||
<div class="col-12 col-lg-3">
|
|
||||||
<template v-for="player in gameStore.players" :key="player.uuid">
|
|
||||||
<div class="card bg-body-secondary p-2">
|
|
||||||
{{ player.name }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
.w-invite-code {
|
|
||||||
width: 12em;
|
|
||||||
}
|
|
||||||
.spinny-size{
|
|
||||||
max-width: 6.66em;
|
|
||||||
max-height: 6.66em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -25,7 +25,7 @@ const editProfileModalId = "edit-profile-modal-on-profile-page";
|
||||||
<img :src="userStore.pfpSource" alt="Your Profile Pic" />
|
<img :src="userStore.pfpSource" alt="Your Profile Pic" />
|
||||||
</div>
|
</div>
|
||||||
<p class="fs-3">
|
<p class="fs-3">
|
||||||
{{ userStore.getUserOutput }}
|
{{ userStore.username }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3">
|
||||||
<button class="btn btn-outline-primary" :data-bs-target="`#${editProfileModalId}`" data-bs-toggle="modal">
|
<button class="btn btn-outline-primary" :data-bs-target="`#${editProfileModalId}`" data-bs-toggle="modal">
|
||||||
|
|
@ -40,7 +40,7 @@ const editProfileModalId = "edit-profile-modal-on-profile-page";
|
||||||
{{ t('profile.yourBoards') }}
|
{{ t('profile.yourBoards') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<BoardSelector :withNewOption="true" />
|
<BoardSelector />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@
|
||||||
"host": {
|
"host": {
|
||||||
"button": "Host",
|
"button": "Host",
|
||||||
"text": "Host a Game",
|
"text": "Host a Game",
|
||||||
"alreadyCreatedBoard": "Already created a board?",
|
"alreadyHostedGome": "Wanna create a board and host a game yourself?",
|
||||||
"startHost": "Start hosting a game and invite some friends to play"
|
"textCode": "Wanna create a board and host a game yourself?"
|
||||||
},
|
},
|
||||||
"board": {
|
"board": {
|
||||||
"label": "Board",
|
"label": "Board",
|
||||||
|
|
@ -79,7 +79,6 @@
|
||||||
"category": {
|
"category": {
|
||||||
"infos": "Category Infos",
|
"infos": "Category Infos",
|
||||||
"label": "No Category | Category | Categories | {count} Categories",
|
"label": "No Category | Category | Categories | {count} Categories",
|
||||||
"custom-color": "Set Custom Color",
|
|
||||||
"name": "Category Name",
|
"name": "Category Name",
|
||||||
"description": "Category Description",
|
"description": "Category Description",
|
||||||
"add": "Add Category",
|
"add": "Add Category",
|
||||||
|
|
@ -99,69 +98,10 @@
|
||||||
"text": "Question Text",
|
"text": "Question Text",
|
||||||
"type": "Question Type",
|
"type": "Question Type",
|
||||||
"fontsize": "Font Size",
|
"fontsize": "Font Size",
|
||||||
"types": {
|
|
||||||
"title": {
|
|
||||||
"0": "Simple Question",
|
|
||||||
"1": "Image Question",
|
|
||||||
"2": "Audio Question",
|
|
||||||
"3": "Location Question"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"0": "A simple question with just text",
|
|
||||||
"1": "A question with text and an image",
|
|
||||||
"2": "A question with text and audio",
|
|
||||||
"3": "A question with an image for players to guess the location on the image"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload": {
|
"upload": {
|
||||||
"image": "Upload an image",
|
"image": "Upload an image",
|
||||||
"audio": "Upload an audio file"
|
"audio": "Upload an audio file"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"answer": {
|
|
||||||
"label": "No Answer | Answer | Answers | {count} Answers",
|
|
||||||
"text": "Answer Text",
|
|
||||||
"types": {
|
|
||||||
"label": "Answer Type",
|
|
||||||
"title": {
|
|
||||||
"0": "Simple Answer",
|
|
||||||
"1": "Image Answer",
|
|
||||||
"2": "Audio Answer",
|
|
||||||
"3": "Location Answer"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"0": "A simple answer with just text",
|
|
||||||
"1": "An answer with text and an image",
|
|
||||||
"2": "An answer with text and audio",
|
|
||||||
"3": "An answer for a location question with the correct location shown on an image"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"game": {
|
|
||||||
"host": {
|
|
||||||
"text": {
|
|
||||||
"seconds": "seconds",
|
|
||||||
"in": "in",
|
|
||||||
"pointsAdjustmentValue": "Value for manual adjustments"
|
|
||||||
},
|
|
||||||
"button": {
|
|
||||||
"lockIn": "Lock",
|
|
||||||
"letChoose": "Let Choose",
|
|
||||||
"letNextChoose": "Let next player choose",
|
|
||||||
"revealAll": "Reveal All Answers",
|
|
||||||
"reveal": "Reveal Answer",
|
|
||||||
"unlock": "Unlock Again",
|
|
||||||
"correct": "Correct",
|
|
||||||
"wrong": "Wrong",
|
|
||||||
"pointsAdd": "Add Points",
|
|
||||||
"pointsSubtract": "Subtract Points"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"notAvailable": "No game found/available",
|
|
||||||
"lobby": "In lobby - not yet startet",
|
|
||||||
"inProgress": "Game in progress"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
@ -171,7 +111,7 @@
|
||||||
"light": {
|
"light": {
|
||||||
"name": "Light"
|
"name": "Light"
|
||||||
},
|
},
|
||||||
"high-contrast": {
|
"highContrast": {
|
||||||
"name": "High Contrast"
|
"name": "High Contrast"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import '@/assets/scss/customized_bootstrap.scss';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faSun, faMoon, faCircleHalfStroke, faEdit, faPlay, faSpinner, faLanguage, faGlobe, faPlus, faTrash, faGripLines, faAngleUp, faAngleDown, faAngleLeft, faAnglesRight, faHandPointer, faMinus, faEye, faLock, faLockOpen, faSave, faXmark, faUsers, faArrowRightToBracket, faCopy, faCheck, faArrowRight, faAlignCenter } from '@fortawesome/free-solid-svg-icons';
|
import { faSun, faMoon, faCircleHalfStroke, faEdit, faPlay, faSpinner, faLanguage, faGlobe, faPlus, faTrash, faGripLines, faAngleUp, faAngleDown, faAngleLeft } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import enMessages from './locales/en.json';
|
import enMessages from './locales/en.json';
|
||||||
import deMessages from './locales/de.json';
|
import deMessages from './locales/de.json';
|
||||||
|
|
@ -34,31 +34,17 @@ library.add(
|
||||||
faLanguage,
|
faLanguage,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
faPlus,
|
faPlus,
|
||||||
faMinus,
|
|
||||||
faTrash,
|
faTrash,
|
||||||
faGripLines,
|
faGripLines,
|
||||||
faAngleUp,
|
faAngleUp,
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faAngleLeft,
|
faAngleLeft,
|
||||||
faAnglesRight,
|
|
||||||
faHandPointer,
|
|
||||||
faEye,
|
|
||||||
faLock,
|
|
||||||
faLockOpen,
|
|
||||||
faSave,
|
|
||||||
faXmark,
|
|
||||||
faUsers,
|
|
||||||
faArrowRightToBracket,
|
|
||||||
faCopy,
|
|
||||||
faCheck,
|
|
||||||
faArrowRight,
|
|
||||||
faAlignCenter,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const app = createApp( App );
|
const app = createApp( App );
|
||||||
|
|
||||||
app.component('FontAwesomeIcon', FontAwesomeIcon);
|
|
||||||
app.use( createPinia() );
|
app.use( createPinia() );
|
||||||
|
app.component( 'FontAwesomeIcon', FontAwesomeIcon );
|
||||||
app.use( router );
|
app.use( router );
|
||||||
app.use( i18n );
|
app.use( i18n );
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import type { AnswerType } from './AnswerType';
|
import type { BoardEntry } from './BoardEntry';
|
||||||
import type { Point } from './Point';
|
|
||||||
|
|
||||||
export class Answer{
|
export class Answer{
|
||||||
constructor(
|
constructor(
|
||||||
public text: string,
|
public text: string,
|
||||||
public answerType: AnswerType,
|
public boardEntry: BoardEntry | undefined,
|
||||||
public location: Point | undefined = undefined,
|
public image: URL | undefined = undefined,
|
||||||
public image: string | undefined = undefined,
|
|
||||||
public id: number | undefined = undefined,
|
public id: number | undefined = undefined,
|
||||||
){}
|
){}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue