login/signup commit

This commit is contained in:
Michael Wain 2024-02-14 15:09:16 +03:00
parent 8386db6502
commit aecd4fd77a
31 changed files with 1756 additions and 8 deletions

103
pom.xml Normal file
View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alterdekim.game</groupId>
<artifactId>NoDice</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
</parent>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<!-- Temporary explicit version to fix Thymeleaf bug -->
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,2 +1,24 @@
package com.alterdekim.game;public class Application {
}
package com.alterdekim.game;
import com.alterdekim.game.storage.StorageService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
CommandLineRunner init(StorageService storageService) {
return (args) -> {
storageService.init();
};
}
}

View File

@ -0,0 +1,83 @@
package com.alterdekim.game.controller;
import com.alterdekim.game.dto.UserDTO;
import com.alterdekim.game.entities.Invite;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.InviteService;
import com.alterdekim.game.service.UserService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Slf4j
@Controller
public class AuthController {
private final String base_title = " | Nosedive";
private final UserService userService;
private final InviteService inviteService;
public AuthController(UserService userService, InviteService inviteService) {
this.inviteService = inviteService;
this.userService = userService;
}
@GetMapping("/game")
public String gamePage(Model model) {
return "game";
}
@GetMapping("/login")
public String loginPage(Model model) {
model.addAttribute("title", "Login" + base_title);
return "login";
}
@GetMapping("/access-denied")
public String accessDenied(Model model) {
model.addAttribute("title", "Access denied");
return "access-denied";
}
@GetMapping("/signup")
public String showRegistrationForm(Model model) {
UserDTO userDto = new UserDTO();
model.addAttribute("user", userDto);
return "signup";
}
@PostMapping("/signup")
public String registration(
@ModelAttribute("user") @Valid UserDTO userDto,
BindingResult result,
Model model) {
User existingUser = userService.findByUsername(userDto.getUsername());
Invite existingInvite = inviteService.findById(1);
if(existingUser != null && existingUser.getUsername() != null && !existingUser.getUsername().isEmpty() ){
result.rejectValue("username", null,
"There is already an account registered with the same username");
return "redirect:/signup?error=1";
}
if(!existingInvite.getInvite_code().equals(userDto.getInvite_code())) {
result.rejectValue("invite_code", null, "Incorrect invite code.");
return "redirect:/signup?error=1";
}
if(result.hasErrors()) {
model.addAttribute("user", new UserDTO());
return "redirect:/signup?error=1";
}
userService.saveUser(userDto);
return "redirect:/";
}
}

View File

@ -0,0 +1,46 @@
package com.alterdekim.game.controller;
import com.alterdekim.game.storage.StorageFileNotFoundException;
import com.alterdekim.game.storage.StorageProperties;
import com.alterdekim.game.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@EnableConfigurationProperties(StorageProperties.class)
public class FileServerController {
private final StorageService storageService;
@Autowired
public FileServerController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping("/static/{assetspath}/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename, @PathVariable String assetspath) {
Resource file = storageService.loadAsResource("static/" + assetspath + "/" + filename);
if( assetspath.equals("images") ) {
return ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFilename() + "\"").body(file);
}
return ResponseEntity.ok().contentType(new MediaType("text", assetspath)).header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFilename() + "\"").body(file);
}
@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}
}

View File

@ -0,0 +1,24 @@
package com.alterdekim.game.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private Long id;
@NotEmpty(message = "Username should not be empty")
private String username;
@NotEmpty(message = "Password should not be empty")
private String password;
@NotEmpty(message = "Invite code should not be empty")
private String invite_code;
private String lang;
}

View File

@ -0,0 +1,23 @@
package com.alterdekim.game.entities;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "invite_codes")
public class Invite {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false)
private String invite_code;
}

View File

@ -0,0 +1,29 @@
package com.alterdekim.game.entities;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="roles")
public class Role
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false, unique=true)
private String name;
@ManyToMany(mappedBy="roles")
private List<User> users;
}

View File

@ -0,0 +1,38 @@
package com.alterdekim.game.entities;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="users")
public class User {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false, unique=true)
private String username;
@Column(nullable=false)
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(
name="users_roles",
joinColumns={@JoinColumn(name="USER_ID", referencedColumnName="ID")},
inverseJoinColumns={@JoinColumn(name="ROLE_ID", referencedColumnName="ID")})
private List<Role> roles = new ArrayList<>();
}

View File

@ -0,0 +1,16 @@
package com.alterdekim.game.handler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import java.io.IOException;
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendRedirect("/access-denied");
}
}

View File

@ -0,0 +1,11 @@
package com.alterdekim.game.repository;
import com.alterdekim.game.entities.Invite;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface InviteRepository extends JpaRepository<Invite, Integer> {
Optional<Invite> findById(Integer id);
}

View File

@ -0,0 +1,13 @@
package com.alterdekim.game.repository;
import com.alterdekim.game.entities.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(String name);
List<Role> findAll();
}

View File

@ -0,0 +1,10 @@
package com.alterdekim.game.repository;
import com.alterdekim.game.entities.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}

View File

@ -0,0 +1,78 @@
package com.alterdekim.game.security;
import com.alterdekim.game.handler.CustomAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SpringSecurity {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName(null);
http.csrf().disable()
.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers("/game").permitAll()
.requestMatchers("/games").permitAll()
.requestMatchers("/static/**").permitAll()
.requestMatchers("/access-denied").permitAll()
.requestMatchers("/signup").permitAll()
.requestMatchers("/favicon.ico").permitAll()
.requestMatchers("/signup/**").permitAll()
.requestMatchers("/").permitAll()
).formLogin(
form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.failureForwardUrl("/")
.defaultSuccessUrl("/game")
.permitAll()
)
.logout(
logout -> logout
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.permitAll()
)
.requestCache((cache) -> cache
.requestCache(requestCache)
)
.exceptionHandling((exc) -> exc
.accessDeniedHandler(accessDeniedHandler())
.accessDeniedPage("/access-denied"));
return http.build();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,7 @@
package com.alterdekim.game.service;
import com.alterdekim.game.entities.Invite;
public interface InviteService {
Invite findById(Integer id);
}

View File

@ -0,0 +1,20 @@
package com.alterdekim.game.service;
import com.alterdekim.game.entities.Invite;
import com.alterdekim.game.repository.InviteRepository;
import org.springframework.stereotype.Service;
@Service
public class InviteServiceImpl implements InviteService {
private final InviteRepository repository;
public InviteServiceImpl(InviteRepository repository) {
this.repository = repository;
}
@Override
public Invite findById(Integer id) {
return repository.findById(id).orElse(null);
}
}

View File

@ -0,0 +1,15 @@
package com.alterdekim.game.service;
import com.alterdekim.game.dto.UserDTO;
import com.alterdekim.game.entities.User;
import java.util.List;
public interface UserService {
void saveUser(UserDTO userDto);
User findByUsername(String usernane);
List<UserDTO> findAllUsers();
}

View File

@ -0,0 +1,68 @@
package com.alterdekim.game.service;
import com.alterdekim.game.dto.UserDTO;
import com.alterdekim.game.entities.Role;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.repository.RoleRepository;
import com.alterdekim.game.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
public UserServiceImpl(UserRepository userRepository,
RoleRepository roleRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public void saveUser(UserDTO userDto) {
User user = new User();
user.setUsername(userDto.getUsername());
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
Role role = roleRepository.findByName("ROLE_ADMIN");
if(role == null){
role = checkRoleExist();
}
user.setRoles(Collections.singletonList(role));
userRepository.save(user);
}
@Override
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
@Override
public List<UserDTO> findAllUsers() {
List<User> users = userRepository.findAll();
return users.stream().map(this::convertEntityToDto)
.collect(Collectors.toList());
}
private UserDTO convertEntityToDto(User user){
UserDTO userDto = new UserDTO();
userDto.setUsername(user.getUsername());
return userDto;
}
private Role checkRoleExist() {
Role role = new Role();
role.setName("ROLE_ADMIN");
return roleRepository.save(role);
}
}

View File

@ -1,2 +1,106 @@
package com.alterdekim.game.storage;public class FileSystemStorageService {
package com.alterdekim.game.storage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;
@Service
public class FileSystemStorageService implements StorageService {
private final Path rootLocation;
@Autowired
public FileSystemStorageService(StorageProperties properties) {
if(properties.getLocation().trim().isEmpty()){
throw new StorageException("File upload location can not be Empty.");
}
this.rootLocation = Paths.get(new ClassPathResource(properties.getLocation()).getPath());
}
@Override
public void store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new StorageException("Failed to store empty file.");
}
Path destinationFile = this.rootLocation.resolve(
Paths.get(file.getOriginalFilename()))
.normalize().toAbsolutePath();
if (!destinationFile.getParent().equals(this.rootLocation.toAbsolutePath())) {
throw new StorageException(
"Cannot store file outside current directory.");
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, destinationFile,
StandardCopyOption.REPLACE_EXISTING);
}
}
catch (IOException e) {
throw new StorageException("Failed to store file.", e);
}
}
@Override
public Stream<Path> loadAll() {
try {
return Files.walk(this.rootLocation, 1)
.filter(path -> !path.equals(this.rootLocation))
.map(this.rootLocation::relativize);
}
catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}
@Override
public Path load(String filename) {
return rootLocation.resolve(filename);
}
@Override
public Resource loadAsResource(String filename) {
try {
Resource resource = new ClassPathResource(filename);
if (resource.exists() || resource.isReadable()) {
return resource;
}
else {
throw new StorageFileNotFoundException(
"Could not read file: " + filename);
}
}
catch (Exception e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}
@Override
public void deleteAll() {
FileSystemUtils.deleteRecursively(rootLocation.toFile());
}
@Override
public void init() {
try {
Files.createDirectories(rootLocation);
}
catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}
}

View File

@ -1,2 +1,12 @@
package com.alterdekim.game.storage;public class StorageException {
package com.alterdekim.game.storage;
public class StorageException extends RuntimeException {
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,2 +1,12 @@
package com.alterdekim.game.storage;public class StorageFileNotFoundException {
}
package com.alterdekim.game.storage;
public class StorageFileNotFoundException extends StorageException {
public StorageFileNotFoundException(String message) {
super(message);
}
public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,2 +1,12 @@
package com.alterdekim.game.storage;public class StorageProperties {
package com.alterdekim.game.storage;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Setter
@Getter
@ConfigurationProperties("storage")
public class StorageProperties {
private String location = "static";
}

View File

@ -1,2 +1,24 @@
package com.alterdekim.game.storage;public class StorageService {
package com.alterdekim.game.storage;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
}

View File

@ -0,0 +1,506 @@
body {
font-size: 14px;
font-family: 'Open Sans',tahoma,arial,sans-serif;
}
._mbtn {
text-align: center;
margin-left: auto;
margin-right: auto;
margin-bottom: auto;
font-size: 12px;
}
.dropbox {
width: 17%;
position: absolute;
margin-top: 2%;
}
.dropbox div {
background-color: white;
color: grey;
font-size: 16pt;
padding-top: 5px;
padding-bottom: 5px;
}
.stars {
position: fixed;
width: 55px;
text-align: center;
display: inline-flex;
margin: auto;
color: white;
}
._star {
text-align: center;
width: 100%;
margin-top: -10px;
width: 100%;
}
.hstars {
margin-left: -40px;
}
._hstar {
margin-top: -50px;
transform: rotate(90deg);
}
._vstar {
margin-top: -110px;
}
._hhstar {
margin-left: 60px;
}
.wrapper {
position: fixed;
top: 0px;
left: 0px;
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
background-color: #262c2f;
overflow: hidden!important;
}
#chatinput {
width: 511px;
height: 24px;
background-color:rgba(0, 0, 0, 0.1);
color: white;
border: none;
outline:none;
font-size:16px;
}
#log {
position: fixed;
overflow-x: hidden;
overflow: scroll;
width: 511px;
height: 480px;
color: white;
}
#loading {
position: fixed;
top: 0px;
left: 0px;
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
background-color: #262c2f;
overflow: hidden!important;
z-index: 99;
}
.table {
display: grid;
grid-column-gap: 2px;
grid-row-gap: 2px;
}
.corner {
background-color: white;
width: 100px;
height: 100px;
}
.fv {
background-color: #f5f5f5;
width: 100px;
height: 55px;
cursor: pointer;
}
.fh {
background-color: #f5f5f5;
width: 55px;
height: 100px;
cursor: pointer;
}
.cost {
background-color: white;
text-align: center;
color: white;
}
/* .cost.red {
background-color: #CC0000;
color:#FFFFFF
}
.cost.green {
background-color: #00CC00;
color:#FFFFFF
} */
.up {
margin-top: 18px;
}
.iconH {
width: 55px;
height: 100px;
position:relative;
}
.iconV {
width: 100px;
height: 55px;
position:relative;
/* background-image: url("http://localhost/monopoly/fields/land_rover.png"); */
}
.horImg {
/* width:55px;
height:55px; */
width:55px;
position:absolute;
left:25px;
}
.vertImg {
/* width:55px;
height:55px; */
width:55px;
-webkit-transform:rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
position:absolute;
top:25px;
}
.rcost {
float: right;
background-color: white;
width: 55px;
height: 18px;
transform: rotate(90deg);
margin-top: 18px;
margin-left: 82px;
text-align: center;
color: white;
}
.lcost {
float: left;
background-color: white;
width: 55px;
height: 18px;
transform: rotate(90deg);
margin-top: 18px;
margin-left: -38px;
text-align: center;
color: white;
}
.table_center {
grid-row-start: 2;
grid-column-start: 2;
grid-column-end: 11;
grid-row-end: 11;
}
.players {
margin-right: 80px;
}
.player {
margin-top: 10px;
background-color: #15191a;
width: 173px;
height: 130px;
color: white;
text-align: center;
font-size: 22px;
padding: 4px;
cursor: pointer;
}
.player-turn {
width: 193px;
height: 150px;
}
.game {
display: inline-flex;
}
/*
chip colors:
red - #f2454a
blue - #00adef
green - #73d249
purple - #c874ed
orange - #e19d22
*/
.font-red { color: #f2454a; }
.font-blue { color: #00adef; }
.font-green { color: #73d249; }
.font-purple { color: #c874ed; }
.font-orange { color: #e19d22; }
.red { background-color: #f2454a; }
.blue { background-color: #00adef; }
.green { background-color: #73d249; }
.purple { background-color: #c874ed; }
.orange { background-color: #e19d22; }
.chips {
/* background-color:#ff0000; */
width:100%;
height:100%;
left:173px;
top:0px;
position:absolute;
/* border:1px solid green; */
}
.chip {
box-shadow: 0 0 10px rgba(0,0,0,1);
border-radius: 50px;
position: absolute;
width: 20px;
height: 20px;
z-index: 98;
grid-row-start: 1;
grid-row-end: 11;
grid-column-start: 1;
grid-column-end: 11;
margin-top: 18px;
}
.chipMask {
border-radius: 50px;
width: 20px;
height: 20px;
z-index: 99;
background-color: black;
opacity: 0.8;
}
.btn_string {
display: inline-flex;
padding-left:5px;
}
#cell_descr {
z-index:100;
border-radius:10px;
width: 202px;
background-color:#ffffff;
position:absolute;
box-shadow: 0px -5px 10px rgba(10,10,10,0.3);
background-color:#ffffff;
padding-bottom:10px;
}
.cell_descr_top {
background-color:#646d79;
width:202px;
height:57px;
border-radius:10px 10px 0px 0px;
}
.rent_table {
color:#646d79;
font-size:11px;
background-color:#ffffff;
margin-left:10px;
margin-right:10px;
}
.currency {
color:#adb3bb;
}
.star {
font-size:14px;
}
.star_big {
color:#fdbd00;
font-size:18px;
}
.cell_descr_title {
color:#ffffff;
font-size:16px;
width:202px;
padding-left: 10px;
padding-top: 10px;
padding-bottom: 4px;
padding-right: 5px;
}
.cell_descr_subtitle {
color:#e0e0e0;
font-size:9px;
width:202px;
padding-left: 10px;
padding-top: 0px;
padding-bottom: 5px;
padding-right: 5px;
}
#dialog {
position: fixed;
width: 491px;
/* height: 200px;*/
background-color: white;
margin-left:10px;
margin-right:10px;
margin-top:10px;
padding-bottom:10px;
border-radius:10px;
color:#646d79;
}
#dialog_title {
font-size: 18px;
margin: 10px;
}
#dialog_desc {
font-size: 12px;
margin: 10px;
}
#contract {
width: 511px;
height: 511px;
background-color: white;
position: fixed;
color: #646d79;
font-size: 20px;
text-align: center;
}
.btn {
border-radius: 5px;
font-size: 12px;
color: white;
width: 220px;
text-align: center;
padding: 6px 4px;
margin: 6px;
cursor: pointer;
}
.btn_long {
border-radius: 5px;
font-size: 12px;
color: white;
width: 460px;
text-align: center;
padding: 6px 4px;
margin: 6px;
cursor: pointer;
}
[contenteditable="true"].single-line {
white-space: nowrap;
width:100px;
overflow: hidden;
}
[contenteditable="true"].single-line br {
display:none;
}
[contenteditable="true"].single-line * {
display:inline;
white-space:nowrap;
}
.btn.green, .btn_long.green {
background-color: #00c29e;
}
.btn.grey, .btn_long.grey {
background-color: #eceef1;
color:#646d79;
}
p.contract-player {
margin-top: 0.5em;
margin-bottom: 1em;
}
.contract_money {
background: transparent;
border: none;
outline: none;
appearance: textfield;
font-size: 20px;
color: #646d79;
}
h2 {
color: white;
width: 100%;
height: 100%;
margin-top: 50%;
text-align: center;
}
.diceFace {
position:absolute;
width:200px;
height:200px;
text-align:center;
line-height:200px;
font-size:45px;
}
.dice_view {
position: absolute;
width:200px;
height:200px;
perspective:600px;
transform: scale(0.5);
}
.dice {
width:200px;
height:200px;
position:relative;
transform-style:preserve-3d;
transition: transform 1s;
}
.dice_front {
background-image: url("monopoly/cube/1.jpg");
background-size: 200px;
transform:rotateY(0deg) translateZ(100px);
}
.dice_right {
background-image: url("monopoly/cube/2.jpg");
background-size: 200px;
transform:rotateY(90deg) translateZ(100px);
}
.dice_back {
background-image: url("monopoly/cube/3.jpg");
background-size: 200px;
transform:rotateY(180deg) translateZ(100px);
}
.dice_left {
background-image: url("monopoly/cube/4.jpg");
background-size: 200px;
transform:rotateY(-90deg) translateZ(100px);
}
.dice_top {
background-image: url("monopoly/cube/5.jpg");
background-size: 200px;
transform:rotateX(90deg) translateZ(100px);
}
.dice_bottom {
background-image: url("monopoly/cube/6.jpg");
background-size: 200px;
transform:rotateX(-90deg) translateZ(100px);
}
.timeout {
position: absolute;
left: 150px;
font-size: 15px;
}

View File

@ -0,0 +1,52 @@
.navbar {
background-color: #fff;
}
html, body {
font-family: 'Montserrat', sans-serif;
background-color: #f4f4f5;
font-size: 0.875rem;
}
footer {
bottom: 0;
width: 100%;
background-color: #e6e9ed;
}
.nav-item {
margin-right: 1rem;
}
.nav-item:last-child {
margin-right: 0;
}
.btn-outline-primary {
border-color: transparent;
color: #5b5d67;
}
.btn-primary {
background-color: #37bc9d;
color: #fff;
border-color: #37bc9d;
}
.btn-primary:hover {
background-color: rgba(55,188,157,0.85);
border-color: rgba(55,188,157,0.85);
}
.btn-outline-primary:hover {
background-color: rgba(55,188,157,0.85);
border-color: rgba(55,188,157,0.85);
}
.navbar-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.2em;
}

View File

@ -0,0 +1,8 @@
<th:block th:fragment="essentials">
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
</script>
</th:block>

View File

@ -0,0 +1,24 @@
<th:block th:fragment="footer">
<footer class="bg-light text-center text-lg-start">
<div class="container p-4">
<div class="row d-flex justify-content-center">
<div class="col-lg-4 col-md-12 mb-4 mb-md-0">
<h6 class="text-uppercase">Nosedive</h6>
<p>
Nosedive is a free online game.
All brands and trademarks on this page belong to their respective owners and are advertised.
</p>
</div>
<div class="mx-auto col-lg-6 col-md-12 mb-4 mb-md-0">
<h6 class="text-uppercase">Social</h6>
<p>
Follow Monopoly on social networks to stay up to date with game updates.
</p>
</div>
</div>
</div>
<div class="text-center p-3">
© 2024 Copyright: alterdekim
</div>
</footer>
</th:block>

View File

@ -0,0 +1,8 @@
<th:block th:fragment="head">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title th:text="${title} ? ${title} : 'Nosedive'"></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,500,800" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
</th:block>

View File

@ -0,0 +1,32 @@
<th:block th:fragment="navbar">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<div class="collapse navbar-collapse justify-content-center" id="navbarNav">
<a class="navbar-brand" href="/">Nosedive</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="navbar-nav">
<li class="nav-item">
<button class="btn btn-primary navbar-btn">
<ion-icon name="game-controller-outline"></ion-icon>
<span>Games</span>
</button>
</li>
<li class="nav-item">
<button class="btn btn-outline-primary navbar-btn">
<ion-icon name="people-outline"></ion-icon>
<span>Friends</span>
</button>
</li>
<li class="nav-item">
<button class="btn btn-outline-primary navbar-btn">
<ion-icon name="briefcase-outline"></ion-icon>
<span>Inventory</span>
</button>
</li>
</ul>
</div>
</div>
</nav>
</th:block>

View File

@ -0,0 +1,272 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"></meta>
<link rel="stylesheet" href="/static/css/game.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<title>Монополия онлайн</title>
<link rel="shortcut icon" type="image/png" href="../static/images/favicon.ico"/>
<div id="contract_field" style="display: none">
<div style="display: inline-flex;">
<div class="contract_image" style="margin-left: 10px"><img src="../static/images/mitsubishi.png" style="width: 55px;"></div>
<div class="contract_info" style="margin-left: 10px; display: inline-block; text-align: left">
<div class="contract_name">Mitsubishi</div>
<div class="contract_cost"style="font-size: 14px">3.000$</div>
</div>
</div>
</div>
</head>
<body>
<!--<div id="loading" style="width: 100%; height: 100%;">
<div style="text-align: center">
<p id="loading_status" style="color: white">Идёт загрузка игры...</p>
<div id="returnBtn" onclick="window.location = '/games?gtype=monopoly'" class="btn green" style="margin-top: 5px; display: none; margin-bottom: auto; margin-left: auto; margin-right: auto;">Назад</div>
</div>
</div>-->
<div class="wrapper" style="width: 100%; height: 100%">
<div class="game">
<div class="players">
<th:block th:each="p: ${players}">
<div class="player" data-pid="${p.id}" onClick="drop(this)">
<p class="timeout"></p>
<p class="nickname" th:text="${p.name}"></p>
<p class="money" th:text="${p.money}"></p>
<div class="dropbox" style="display: none"></div> <!-- margin-top: -35px; -->
</div>
</th:block>
</div>
<div class="table">
<div class="table_center">
<!--<div id="log" style="position: fixed; padding: 5px; overflow: scroll;">
<div style="display: inline-flex;"><p class="font-red">Name: </p><p> lol</p></div>
<div style="display: inline-flex;"><p class="font-red">Name: </p><p> lol</p></div>
<div style="display: inline-flex;"><p class="font-red">Name: </p><p> lol</p></div>
<div style="display: inline-flex;"><p class="font-red">Name: </p><p> lol</p></div>
</div> -->
<div id="log">
<!-- <div>Игрок <span style="color: red">lol</span> ходит</div> -->
</div>
<div style="position: absolute; margin-top: 485px" >
<input placeholder="Напишите в чат" type="text" id="chatinput"/>
</div>
<div id="dice_cubes" style="margin-top: 100px; display: none;">
<div class="dice_view" style="margin-left: 100px;">
<div class="dice" id="first_dice">
<div class="diceFace dice_front"></div>
<div class="diceFace dice_right"></div>
<div class="diceFace dice_back"></div>
<div class="diceFace dice_left"></div>
<div class="diceFace dice_top"></div>
<div class="diceFace dice_bottom"></div>
</div>
</div>
<div class="dice_view" style="margin-left: 225px; margin-top: 125px">
<div class="dice" id="second_dice">
<div class="diceFace dice_front"></div>
<div class="diceFace dice_right"></div>
<div class="diceFace dice_back"></div>
<div class="diceFace dice_left"></div>
<div class="diceFace dice_top"></div>
<div class="diceFace dice_bottom"></div>
</div>
</div>
</div>
<div id="contract" style="display: none;">
<p>Договор</p>
<div style="display: inline-flex; width: 511px">
<p style="font-size: 16px; text-align: left; margin-left: 10px" class="font-red contract-player">Вы: </p>
<p style="font-size: 16px; text-align: left; margin-left: 230px" class="font-green contract-player">User: </p>
</div>
<div id="my_contract_fields" style="position: absolute; display: grid">
<div style="display: inline-flex;">
<div style="margin-left: 10px"><img src="../static/images/money.png" style="width: 55px;"></div>
<div style="margin-left: 10px; display: inline-block; text-align: left">
<input type="number" id="self_money" class="contract_money single-line" value="0" />
<div style="font-size: 14px">Наличные</div>
</div>
</div>
<!--<div style="display: inline-flex;">
<div style="margin-left: 10"><img src="images/mitsubishi.png" style="width: 55px;"></div>
<div style="margin-left: 10; display: inline-block; text-align: left">
<div>Mitsubishi</div>
<div style="font-size: 14">3.000$</div>
</div>
</div>-->
</div>
<div id="contract_fields" style="position: absolute; display: grid; margin-left: 260px">
<div style="display: inline-flex;">
<div style="margin-left: 10px"><img src="../static/images/money.png" style="width: 55px;"></div>
<div style="margin-left: 10px; display: inline-block; text-align: left">
<input type="number" id="other_money" class="contract_money single-line" value="0" />
<div style="font-size: 14px">Наличные</div>
</div>
</div>
</div>
<hr width="1" size="320" style="height: 60%">
<div style="display: flex; width: 100%">
<div style="width: 33.3%">
<div id="allSelfMoney" style="font-size: 16px;">0$</div>
</div>
<div style="width: 33.3%">
<div style="font-size: 16px;">Общая сумма</div>
</div>
<div style="width: 33.3%">
<div id="allOtherMoney" style="font-size: 16px;">0$</div>
</div>
</div>
<div class="btn_string">
<div id="contractAgree" onClick="contractAgree()" class="btn green" style="width: 100px;">Принять</div><div id="contractDeny" onClick="contractDeny()" class="btn red" style="width: 100px;">Отказать</div>
</div>
</div>
<div style="display: none;" id="dialog" class="dialogturn">
<p id="dialog_title">Покупаем?</p>
<p id="dialog_desc">Lorem ipsum dolor sit amet...</p></br>
<div class="btn_string" id="twobtns">
<div id="buybtn" onClick="leftBtnClicked(this)" class="btn green">Купить за 1,000k</div><div id="ignorebtn" onClick="rightBtnClicked()" class="btn grey">На аукцион</div>
</div>
<div class="btn_string" id="onebtn">
<div class="btn_long green" onClick="centerBtnClicked()" id="ogbtn">Купить за 1,000k</div>
</div>
</div>
<!-- style="top:270px;left:400px" -->
<div style="display: none;" id="cell_descr">
<div class="cell_descr_top">
<div class="cell_descr_title">Apple</div>
<div class="cell_descr_subtitle"><!--Электроника--></div>
</div>
<table class="rent_table">
<tr>
<div id="buyOrSell" style="display: none;" >
<div style="width: 100%; display: inline-flex;">
<div onClick="bnf(this)" id="tbbb" class="btn green" style="width: 100%">Купить</div><div onClick="sfof(this)" id="tbsb" style="width: 100%" class="btn red">Продать</div>
</div>
</div>
<div id="onlySell" style="display: none;">
<div style="width: 100%; display: inline-flex;">
<div class="btn_long red" onClick="sfof(this)">Продать</div>
</div>
</div>
</tr>
<tr>
<td colspan="2">Стройте филиалы, чтобы увеличить ренту</td>
</tr>
<tr>
<td>Базовая рента</td>
<td align="right"><b id="baserent">350</b><span class="currency">k</span></td>
</tr>
<tr>
<td><span class="star">&#9733;</span></td>
<td align="right"><b id="firstrent">1,750</b><span class="currency">k</span></td>
</tr>
<tr>
<td><span class="star">&#9733; &#9733;</span></td>
<td align="right"><b id="secondrent">5,000</b><span class="currency">k</span></td>
</tr>
<tr>
<td><span class="star">&#9733; &#9733; &#9733;</span></td>
<td align="right"><b id="thirdrent">11,050</b><span class="currency">k</span></td>
</tr>
<tr>
<td><span class="star">&#9733; &#9733; &#9733; &#9733;</span></td>
<td align="right"><b id="fourthrent">13,000</b><span class="currency">k</span></td>
</tr>
<tr>
<td><span class="star_big">&#9733;</span></td>
<td align="right"><b id="fithrent">15,000</b><span class="currency">k</span></td>
</tr>
<tr>
<td style="padding-top:10px;">Стоимость поля</td>
<td style="padding-top:10px;" align="right"><b id="buyprice">3,500</b><span class="currency">k</span></td>
</tr>
<tr style="visibility: hidden;">
<td>Залог поля</td>
<td align="right"><b id="depositprice">1,750</b><span class="currency">k</span></td>
</tr>
<tr style="visibility: hidden;">
<td>Выкуп поля</td>
<td align="right"><b id="redeemprice">2,100</b><span class="currency">k</span></td>
</tr>
<tr>
<td>Покупка филиала</td>
<td align="right"><b id="fprice">2,000</b><span class="currency">k</span></td>
</tr>
</table>
</div>
</div>
<!-- <div class="chips"> -->
<!-- {262,55} {318,55} {375,55} -->
<div class="chip red" data-trow=0 data-tcol=0></div>
<div class="chip green" data-trow=0 data-tcol=0></div>
<div class="chip blue" data-trow=0 data-tcol=0></div>
<div class="chip purple" data-trow=0 data-tcol=0></div>
<div class="chip orange" data-trow=0 data-tcol=0></div>
<!-- </div> -->
<div class="up"><div class="board_field corner"><img src="../static/images/start.png" style="width: 100%; height: 100%;" /></div></div>
<th:block th:each="f: ${fields_up}">
<div th:data-fid="${f.uid}" class="board_field" style="grid-column: @{${f.id}}"> <!-- 2 + -->
<div class="cost" th:text="${f.cost}"></div>
<div class="fh">
<div class="iconH">
<img th:src="${f.img}" class="vertImg">
</div>
<div class="stars">
<span class="_star" th:text="${f.stars}"></span>
</div>
</div>
</div>
</th:block>
<div class="up" style="grid-column: 11"><div class="board_field corner"><img src="../static/images/injail.png" style="width: 100%; height: 100%;" /></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/lacoste.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/mitsubishi.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/vw.png" class="horImg"></div></div><div class="stars hstars"><span class="_star _hstar"></span></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/mcdonalds.svg" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/apple.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/vw.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/macdonalds.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/nike.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/pepsi.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field corner" style="grid-column: 11"><img src="../static/images/parking.png" style="width:100%; height:100%;" /></div>
<div class="board_field" style="grid-column: 2; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/nike.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 3; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/mitsubishi.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 4; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/peugeot.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 5; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/vw.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 6; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/linux.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 7; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/apple.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 8; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/burger_king.svg" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 9; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/lacoste.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 10; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/pepsi.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field corner" style="grid-column: 1; grid-row: 11"><img src="../static/images/gotojail.png" style="width: 100%; height: 100%;" /></div>
<div class="board_field" style="grid-column: 1; grid-row: 10;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/peugeot.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 9;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/apple.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 8;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/linux.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 7;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/macdonalds.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 6;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/pepsi.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 5;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/burger_king.svg" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 4;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/mitsubishi.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 3;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/adidas.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 2;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/lacoste.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
</div>
</div>
</div>
</body>
<footer>
<script type="text/javascript" src="/games/cookie.js"></script>
<script type="text/javascript" src="/games/db.js"></script>
<script type="text/javascript" src="/games/methods.js"></script>
<script type="text/javascript" src="game.js"></script>
<script type="text/javascript" src="/static/javascript/scale.js"></script>
</footer>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<th:block th:insert="~{fragments/head}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/navbar}"></th:block>
<div class="container mt-5 min-vh-100" style="max-width: 330px;">
<h4 class="text-center">Login</h4>
<th:block th:if="${param.error}">
<div class="alert alert-danger" role="alert" th:text="#{login_error}"></div>
</th:block>
<th:block th:if="${error}">
<div class="alert alert-danger" role="alert" th:text="#{login_error}"></div>
</th:block>
<form method="post" th:action="@{/login}">
<div class="mb-3">
<label for="username" class="form-label"><ion-icon name="at-outline"></ion-icon> Nickname</label>
<input type="text" name="username" class="form-control" id="username">
</div>
<div class="mb-3">
<label for="password" class="form-label"><ion-icon name="key-outline"></ion-icon> Password</label>
<input type="password" name="password" class="form-control" id="password">
</div>
<div class="mb-3">
<p>Don't have an account? <a href="/signup">Sign up</a></p>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
<th:block th:insert="~{fragments/footer}"></th:block>
<th:block th:insert="~{fragments/essentials}"></th:block>
</body>
</html>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<th:block th:insert="~{fragments/head}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/navbar}"></th:block>
<div class="container mt-5 min-vh-100" style="max-width: 330px;">
<h4 class="text-center">Sign up</h4>
<th:block th:if="${param.error}">
<div class="alert alert-danger" role="alert" th:text="#{login_error}"></div>
</th:block>
<th:block th:if="${error}">
<div class="alert alert-danger" role="alert" th:text="#{login_error}"></div>
</th:block>
<form method="post" th:action="@{/signup}" th:object="${user}">
<div class="mb-3">
<label for="username" class="form-label" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="This nickname will see other users and they will be able tag you like: @username"><ion-icon name="at-outline"></ion-icon> Nickname</label>
<input type="text" name="username" class="form-control" id="username">
</div>
<div class="mb-3">
<label for="password" class="form-label" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Password must have at least 7 symbols, include letters, digits, any of special symbols (#$%:;~|&^!_\/-+*)"><ion-icon name="key-outline"></ion-icon> Password</label>
<input type="password" name="password" class="form-control" id="password">
</div>
<div class="mb-3">
<label for="invite_input">Invite code</label>
<input type="text" name="invite_code" class="form-control" id="invite_input">
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
I have read the terms and conditions
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Sign up</button>
<div class="mb-3">
<p>Already have an account? <a href="/login">Login</a></p>
</div>
</form>
</div>
<th:block th:insert="~{fragments/footer}"></th:block>
<th:block th:insert="~{fragments/essentials}"></th:block>
</body>
</html>