Compare commits

...

10 commits

124 changed files with 10094 additions and 1647 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
tmp/*
sql/CreateBackendUser.sql

3
backend/.gitignore vendored
View file

@ -22,3 +22,6 @@ src/main/resources/application-local.properties
# Server Keystore
src/main/resources/achievements-ssl-key.p12
# Program Data
storage/

View file

@ -1,20 +1,27 @@
package achievements;
import achievements.services.DbConnectionService;
import achievements.misc.DbConnection;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootApplication
import java.time.Duration;
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@EnableScheduling
public class Application {
public static void main(String[] args) {
var context = SpringApplication.run(Application.class, args);
// Verify the database connection succeeded
var db = context.getBean(DbConnectionService.class);
var db = context.getBean(DbConnection.class);
if (db.getConnection() == null) {
SpringApplication.exit(context, () -> 0);
}
@ -25,10 +32,15 @@ public class Application {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/*")
.allowedOrigins("*");
registry
.addMapping("/**")
.allowedOrigins("*");
}
};
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View file

@ -0,0 +1,17 @@
package achievements.apis;
import achievements.data.APIResponse;
import org.springframework.web.client.RestTemplate;
public abstract class PlatformAPI {
protected int id;
protected RestTemplate rest;
protected PlatformAPI(int id, RestTemplate rest) {
this.id = id;
this.rest = rest;
}
public abstract APIResponse get(String userId);
}

View file

@ -0,0 +1,113 @@
package achievements.apis;
import achievements.apis.steam.GetOwnedGameBody;
import achievements.apis.steam.GetPlayerAchievementsBody;
import achievements.apis.steam.GetSchemaForGameBody;
import achievements.data.APIResponse;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Properties;
public class SteamAPI extends PlatformAPI {
private String apiKey;
public SteamAPI(int id, RestTemplate rest) {
super(id, rest);
try {
var file = new FileInputStream("storage/apis/" + id + ".properties");
var properties = new Properties();
properties.load(file);
apiKey = properties.getProperty("api-key");
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public APIResponse get(String userId) {
var headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
var entity = new HttpEntity<String>(headers);
var ownedGamesUrl = UriComponentsBuilder.fromHttpUrl("http://api.steampowered.com/IPlayerService/GetOwnedGames/v1/")
.queryParam("key", apiKey)
.queryParam("steamid", userId)
.queryParam("include_appinfo", true)
.queryParam("include_played_free_games", true)
.toUriString();
var gameSchemaBaseUrl = UriComponentsBuilder.fromHttpUrl("https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/")
.queryParam("key", apiKey);
var playerAchievementsBaseUrl = UriComponentsBuilder.fromHttpUrl("https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v1/")
.queryParam("key", apiKey)
.queryParam("steamid", userId);
var games = new ArrayList<APIResponse.Game>();
try {
var ownedResponse = rest.exchange(ownedGamesUrl, HttpMethod.GET, entity, GetOwnedGameBody.class).getBody();
for (var game : ownedResponse.getResponse().getGames()) {
var newGame = new APIResponse.Game();
newGame.setPlatformGameId(Integer.toString(game.getAppid()));
newGame.setName(game.getName());
// Technically this is not the advertised logo url, but it's used be steamcommunity.com
// and steamdb.info and it gives better aspect ratios and it means I don't need the random
// logo_url field
newGame.setThumbnail("https://cdn.cloudflare.steamstatic.com/steam/apps/" + game.getAppid() + "/header.jpg");
newGame.setPlayed(game.getPlaytime_forever() > 0);
var achievements = new HashMap<String, APIResponse.Game.Achievement>();
var gameSchemaUrl = gameSchemaBaseUrl.cloneBuilder()
.queryParam("appid", game.getAppid())
.toUriString();
var playerAchievementsUrl = playerAchievementsBaseUrl.cloneBuilder()
.queryParam("appid", game.getAppid())
.toUriString();
try {
var schemaResponse = rest.exchange(gameSchemaUrl, HttpMethod.GET, entity, GetSchemaForGameBody.class).getBody().getGame().getAvailableGameStats();
if (schemaResponse != null && schemaResponse.getAchievements() != null) {
for (var schema : schemaResponse.getAchievements()) {
var achievement = new APIResponse.Game.Achievement();
achievement.setName(schema.getDisplayName());
achievement.setDescription(schema.getDescription());
achievement.setStages(1);
achievement.setThumbnail(schema.getIcon());
achievements.put(schema.getName(), achievement);
}
var playerAchievementsResponse = rest.exchange(playerAchievementsUrl, HttpMethod.GET, entity, GetPlayerAchievementsBody.class).getBody().getPlayerstats().getAchievements();
for (var achievement : playerAchievementsResponse) {
achievements.get(achievement.getApiname()).setProgress(achievement.getAchieved());
}
newGame.setAchievements(new ArrayList<>(achievements.values()));
if (newGame.getAchievements().size() > 0) {
games.add(newGame);
}
}
} catch (Exception e) {
System.err.println("Forbidden APPID: " + game.getAppid());
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
var response = new APIResponse();
response.setGames(games);
return response;
}
}

View file

@ -0,0 +1,96 @@
package achievements.apis.steam;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class GetOwnedGameBody {
public static class Response {
public static class Game {
@JsonProperty("appid")
private int appid;
@JsonProperty("name")
private String name;
@JsonProperty("playtime_forever")
private int playtime_forever;
@JsonProperty("img_icon_url")
private String img_icon_url;
@JsonProperty("img_logo_url")
private String img_logo_url;
public int getAppid() {
return appid;
}
public void setAppid(int appid) {
this.appid = appid;
}
public int getPlaytime_forever() {
return playtime_forever;
}
public void setPlaytime_forever(int playtime_forever) {
this.playtime_forever = playtime_forever;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getImg_icon_url() {
return img_icon_url;
}
public void setImg_icon_url(String img_icon_url) {
this.img_icon_url = img_icon_url;
}
public String getImg_logo_url() {
return img_logo_url;
}
public void setImg_logo_url(String img_logo_url) {
this.img_logo_url = img_logo_url;
}
}
@JsonProperty("game_count")
private int game_count;
@JsonProperty("games")
private List<Game> games;
public int getGame_count() {
return game_count;
}
public void setGame_count(int game_count) {
this.game_count = game_count;
}
public List<Game> getGames() {
return games;
}
public void setGames(List<Game> games) {
this.games = games;
}
}
@JsonProperty("response")
private Response response;
public Response getResponse() {
return response;
}
public void setResponse(Response response) {
this.response = response;
}
}

View file

@ -0,0 +1,54 @@
package achievements.apis.steam;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class GetPlayerAchievementsBody {
public static class PlayerStats {
public static class Achievement {
@JsonProperty("apiname")
private String apiname;
@JsonProperty("achieved")
private int achieved;
public String getApiname() {
return apiname;
}
public void setApiname(String apiname) {
this.apiname = apiname;
}
public int getAchieved() {
return achieved;
}
public void setAchieved(int achieved) {
this.achieved = achieved;
}
}
@JsonProperty("achievements")
private List<Achievement> achievements;
public List<Achievement> getAchievements() {
return achievements;
}
public void setAchievements(List<Achievement> achievements) {
this.achievements = achievements;
}
}
@JsonProperty("playerstats")
private PlayerStats playerstats;
public PlayerStats getPlayerstats() {
return playerstats;
}
public void setPlayerstats(PlayerStats playerstats) {
this.playerstats = playerstats;
}
}

View file

@ -0,0 +1,87 @@
package achievements.apis.steam;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class GetSchemaForGameBody {
public static class Game {
public static class GameStats {
public static class Achievement {
@JsonProperty("name")
private String name;
@JsonProperty("displayName")
private String displayName;
@JsonProperty("description")
private String description;
@JsonProperty("icon")
private String icon;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
}
@JsonProperty("achievements")
private List<Achievement> achievements;
public List<Achievement> getAchievements() {
return achievements;
}
public void setAchievements(List<Achievement> achievements) {
this.achievements = achievements;
}
}
@JsonProperty("availableGameStats")
private GameStats availableGameStats;
public GameStats getAvailableGameStats() {
return availableGameStats;
}
public void setAvailableGameStats(GameStats availableGameStats) {
this.availableGameStats = availableGameStats;
}
}
@JsonProperty("game")
private Game game;
public Game getGame() {
return game;
}
public void setGame(Game game) {
this.game = game;
}
}

View file

@ -0,0 +1,60 @@
package achievements.controllers;
import achievements.data.APError;
import achievements.data.request.RateAchievement;
import achievements.services.ImageService;
import achievements.services.AchievementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/achievement")
public class AchievementController {
@Autowired
private AchievementService achievementService;
@Autowired
private ImageService imageService;
@GetMapping(value = "/{achievement}", produces = "application/json")
public ResponseEntity getAchievement(@PathVariable("achievement") int achievementId) {
var achievement = achievementService.getAchievement(achievementId);
if (achievement == null) {
return ResponseEntity.badRequest().body(new APError(1, "Failed to get achievement"));
} else {
return ResponseEntity.ok(achievement);
}
}
@GetMapping(value = "/{achievement}/image")
public void getProfilePicture(@PathVariable("achievement") int achievement, HttpServletResponse response) {
var icon = achievementService.getIcon(achievement);
imageService.send(icon, "achievement", response);
}
@GetMapping(value = "/{achievement}/rating/{user}")
public ResponseEntity getRating(@PathVariable("achievement") int achievement, @PathVariable("user") int user) {
var rating = achievementService.getRating(achievement, user);
if (rating == null) {
return ResponseEntity.badRequest().body("{}");
} else {
return ResponseEntity.ok(rating);
}
}
@PostMapping(value = "/{achievement}/rating/{user}")
public ResponseEntity setRating(@PathVariable("achievement") int achievement, @PathVariable("user") int user, @RequestBody RateAchievement rating) {
var review = achievementService.setRating(achievement, user, rating);
if (review == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("{}");
} else if (review.getSessionKey() == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(review);
} else {
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
}
}
}

View file

@ -0,0 +1,87 @@
package achievements.controllers;
import achievements.data.APError;
import achievements.data.Session;
import achievements.data.User;
import achievements.services.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationService authService;
/**
* Acceptable codes
* 0 => Success
* 1 => Email already registered
*
* -1 => Unknown error
*/
@PostMapping(value = "/create_user", consumes = "application/json", produces = "application/json")
public ResponseEntity createUser(@RequestBody User user) {
var response = authService.createUser(user);
if (response.status == 0) {
return ResponseEntity.status(HttpStatus.CREATED).body(response.session);
} else if (response.status > 0) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(response.status));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new APError(response.status));
}
}
/**
* DO NOT RETURN CODE DIRECTLY!
*
* User should only ever recieve -1, 0, or 1. The specific authentication error should be hidden.
*
* Acceptable codes
* 0 => Success
* 1 => Unregistered email address
* 2 => Incorrect password
*
* -1 => Unknown error
*/
@PostMapping(value = "/login", consumes = "application/json", produces = "application/json")
public ResponseEntity login(@RequestBody User user) {
var response = authService.login(user);
if (response.status == 0) {
return ResponseEntity.ok(response.session);
} else if (response.status > 0) {
// Hardcoded 1 response code
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(1));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new APError(response.status));
}
}
@PostMapping(value = "/refresh", consumes = "application/json", produces = "application/json")
public ResponseEntity refresh(@RequestBody Session key) {
if (key.getId() == -1) {
if (authService.openAuth()) {
if (authService.refresh(key)) {
return ResponseEntity.ok(key);
} else {
return ResponseEntity.ok(authService.session().generate(-1, 0, true));
}
}
} else if (authService.refresh(key)) {
return ResponseEntity.ok(key);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{}");
}
@PostMapping(value = "/logout", consumes = "application/json")
public ResponseEntity logout(@RequestBody Session session) {
authService.logout(session);
return ResponseEntity.ok("{}");
}
}

View file

@ -1,65 +0,0 @@
package achievements.controllers;
import achievements.data.Achievements;
import achievements.data.Games;
import achievements.data.InternalError;
import achievements.services.DbService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
@RestController
public class Controller {
@Autowired
private DbService db;
public Controller() {}
@RequestMapping(value = { "/achievements", "/achievements/{Name}" }, method = GET, produces = "application/json")
public ResponseEntity<String> fetchAchievements(@PathVariable(value = "Name", required = false) String getName) {
var achievements = (Achievements) null;
if (getName == null) {
achievements = db.getAchievements("%");
} else {
achievements = db.getAchievements(getName);
}
var mapper = new ObjectMapper();
try {
if (achievements == null) {
return new ResponseEntity(mapper.writeValueAsString(new InternalError("Could not get achievements from database")), HttpStatus.INTERNAL_SERVER_ERROR);
} else {
return new ResponseEntity(mapper.writeValueAsString(achievements), HttpStatus.OK);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
return new ResponseEntity("{}", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@RequestMapping(value = { "/games", "/games/{Name}" }, method = GET, produces = "application/json")
public ResponseEntity<String> fetchGames(@PathVariable(value = "Name", required = false) String getName) {
var games = (Games) null;
if (getName == null) {
games = db.getGames("%");
} else {
games = db.getGames(getName);
}
var mapper = new ObjectMapper();
try {
if (games == null) {
return new ResponseEntity(mapper.writeValueAsString(new InternalError("Could not get games from database")), HttpStatus.INTERNAL_SERVER_ERROR);
} else {
return new ResponseEntity(mapper.writeValueAsString(games), HttpStatus.OK);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
return new ResponseEntity("{}", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -0,0 +1,27 @@
package achievements.controllers;
import achievements.services.ImageService;
import achievements.services.GameService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/game")
public class GameController {
@Autowired
private GameService gameService;
@Autowired
private ImageService imageService;
@GetMapping(value = "/{game}/image")
public void getProfilePicture(@PathVariable("game") int game, HttpServletResponse response) {
var icon = gameService.getIcon(game);
imageService.send(icon, "game", response);
}
}

View file

@ -0,0 +1,47 @@
package achievements.controllers;
import achievements.data.importing.ImportPlatform;
import achievements.data.importing.ImportUser;
import achievements.data.importing.ImportUserPlatform;
import achievements.services.ImportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/import")
public class ImportController {
@Autowired
private ImportService importService;
@PostMapping(value = "/platform", consumes = "application/json", produces = "application/json")
public ResponseEntity createPlatform(@RequestBody ImportPlatform platform) {
var response = importService.importPlatform(platform);
if (response == 0) {
return ResponseEntity.ok("{}");
} else {
return ResponseEntity.badRequest().body("{}");
}
}
@PostMapping(value = "/user", consumes = "application/json", produces = "application/json")
public ResponseEntity createUser(@RequestBody ImportUser user) {
var response = importService.importUser(user);
if (response == 0) {
return ResponseEntity.ok("{}");
} else {
return ResponseEntity.badRequest().body("{}");
}
}
@PostMapping(value = "/user/platform", consumes = "application/json", produces = "application/json")
public ResponseEntity addUserToPlatform(@RequestBody ImportUserPlatform userPlatform) {
var response = importService.importUserPlatform(userPlatform);
if (response == 0) {
return ResponseEntity.ok("{}");
} else {
return ResponseEntity.badRequest().body("{}");
}
}
}

View file

@ -0,0 +1,27 @@
package achievements.controllers;
import achievements.services.ImageService;
import achievements.services.PlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/platform")
public class PlatformController {
@Autowired
private ImageService imageService;
@Autowired
private PlatformService platformService;
@GetMapping(value = "/{platform}/image")
public void getIcon(@PathVariable("platform") int platform, HttpServletResponse response) {
var icon = platformService.getIcon(platform);
imageService.send(icon, "platform", response);
}
}

View file

@ -0,0 +1,48 @@
package achievements.controllers;
import achievements.data.request.SearchAchievements;
import achievements.data.request.SearchGames;
import achievements.data.request.SearchUsers;
import achievements.services.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SearchController {
@Autowired
private SearchService searchService;
@PostMapping(value = "/achievements", consumes = "application/json", produces = "application/json")
public ResponseEntity searchAchievements(@RequestBody SearchAchievements searchAchievements) {
var achievements = searchService.searchAchievements(searchAchievements);
if (achievements != null) {
return ResponseEntity.ok(achievements);
} else {
return ResponseEntity.badRequest().body("[]");
}
}
@PostMapping(value = "/users", consumes = "application/json", produces = "application/json")
public ResponseEntity searchAchievements(@RequestBody SearchUsers searchUsers) {
var users = searchService.searchUsers(searchUsers);
if (users != null) {
return ResponseEntity.ok(users);
} else {
return ResponseEntity.badRequest().body("[]");
}
}
@PostMapping(value = "/games", consumes = "application/json", produces = "application/json")
public ResponseEntity searchAchievements(@RequestBody SearchGames searchGames) {
var users = searchService.searchGames(searchGames);
if (users != null) {
return ResponseEntity.ok(users);
} else {
return ResponseEntity.badRequest().body("[]");
}
}
}

View file

@ -0,0 +1,102 @@
package achievements.controllers;
import achievements.data.APError;
import achievements.data.APPostRequest;
import achievements.data.request.AddPlatform;
import achievements.data.request.RemovePlatform;
import achievements.data.request.SetUsername;
import achievements.services.ImageService;
import achievements.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private ImageService imageService;
@GetMapping(value = "/{user}", produces = "application/json")
public ResponseEntity getProfile(@PathVariable("user") int user) {
var profile = userService.getProfile(user);
if (profile == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new APError(1, "Failed to get user profile"));
} else {
return ResponseEntity.ok(profile);
}
}
@PostMapping(value = "/{user}/username", consumes = "application/json", produces = "application/json")
public ResponseEntity setUsername(@PathVariable("user") int userId, @RequestBody SetUsername username) {
var name = userService.setUsername(userId, username);
if (name == 0) {
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
}
return ResponseEntity.badRequest().body("{}");
}
@GetMapping(value = "/{user}/image")
public void getProfilePicture(@PathVariable("user") int user, HttpServletResponse response) {
var profileImage = userService.getProfileImage(user);
imageService.send(profileImage, "user", response);
}
@PostMapping(value = "/{user}/image", consumes = "multipart/form-data", produces = "application/json")
public ResponseEntity setProfilePicture(@PathVariable("user") int user, @RequestPart APPostRequest session, @RequestPart MultipartFile file) {
try {
var type = userService.setProfileImage(user, session.getKey(), file);
if ("not_an_image".equals(type)) {
return ResponseEntity.badRequest().body("{ \"code\": 1, \"message\": \"Not an image type\" }");
} else if ("unsupported_type".equals(type)) {
return ResponseEntity.badRequest().body("{ \"code\": 1, \"message\": \"Unsupported file type\" }");
} else if ("forbidden".equals(type)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("{ \"code\": 2, \"message\": \"Invalid credentials\" }");
} else if ("success".equals(type)) {
return ResponseEntity.status(HttpStatus.CREATED).body("{ \"code\": 0, \"message\": \"Success\" }");
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": -1, \"message\": \"Unknown error\" }");
}
@PostMapping(value = "/{user}/platforms/add", consumes = "application/json", produces = "application/json")
public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatform request) {
var result = userService.addPlatform(userId, request, true);
if (result == 0) {
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
} else {
return ResponseEntity.badRequest().body("{}");
}
}
@PostMapping(value = "/{user}/platforms/remove", consumes = "application/json", produces = "application/json")
public ResponseEntity removePlatformForUser(@PathVariable("user") int userId, @RequestBody RemovePlatform request) {
var result = userService.removePlatform(userId, request);
if (result == 0) {
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
} else {
return ResponseEntity.badRequest().body("{}");
}
}
@GetMapping(value = "/{user}/noteworthy", produces = "application/json")
public ResponseEntity getNoteworthy(@PathVariable("user") int userId) {
var result = userService.getNoteworthy(userId);
if (result != null) {
return ResponseEntity.ok(result);
} else {
return ResponseEntity.badRequest().body("{}");
}
}
}

View file

@ -0,0 +1,36 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public class APError {
@JsonProperty("code")
private int code;
@JsonProperty("message")
private String message;
public APError(int code) {
this.code = code;
}
public APError(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View file

@ -0,0 +1,126 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class APIResponse {
public static class Game {
public static class Achievement {
@JsonProperty("name")
private String name;
@JsonProperty("description")
private String description;
@JsonProperty("stages")
private int stages;
@JsonProperty("progress")
private int progress;
@JsonProperty("thumbnail")
private String thumbnail;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getStages() {
return stages;
}
public void setStages(int stages) {
this.stages = stages;
}
public int getProgress() {
return progress;
}
public void setProgress(int progress) {
this.progress = progress;
}
public String getThumbnail() {
return thumbnail;
}
public void setThumbnail(String thumbnail) {
this.thumbnail = thumbnail;
}
}
@JsonProperty("platformGameId")
private String platformGameId;
@JsonProperty("name")
private String name;
@JsonProperty("played")
private boolean played;
@JsonProperty("thumbnail")
private String thumbnail;
@JsonProperty("achievements")
private List<Achievement> achievements;
public String getPlatformGameId() {
return platformGameId;
}
public void setPlatformGameId(String platformGameId) {
this.platformGameId = platformGameId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isPlayed() {
return played;
}
public void setPlayed(boolean played) {
this.played = played;
}
public String getThumbnail() {
return thumbnail;
}
public void setThumbnail(String thumbnail) {
this.thumbnail = thumbnail;
}
public List<Achievement> getAchievements() {
return achievements;
}
public void setAchievements(List<Achievement> achievements) {
this.achievements = achievements;
}
}
@JsonProperty("games")
private List<Game> games;
public List<Game> getGames() {
return games;
}
public void setGames(List<Game> games) {
this.games = games;
}
}

View file

@ -0,0 +1,17 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public class APPostRequest {
@JsonProperty("key")
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}

View file

@ -1,64 +0,0 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
public class Achievements {
public static class Achievement {
@JsonProperty("name")
private String name;
@JsonProperty("description")
private String description;
@JsonProperty("stages")
private int stages;
public Achievement(String name, String description, int stages) {
this.name = name;
this.description = description;
this.stages = stages;
}
// Start Getters/Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public int getStages() { return stages; }
public void setStages(int stages) { this.stages = stages; }
// End Getters/Setters
}
@JsonProperty("gameID")
private int gameID;
@JsonProperty("gameName")
private String gameName;
@JsonProperty("achievements")
private List<Achievement> achievements;
public Achievements() { achievements = new ArrayList<Achievement>(); }
// Start Getters/Setters
public int getGameID() { return gameID; }
public void setGameID(int gameID) { this.gameID = gameID; }
public String getGameName() { return gameName; }
public void setGameName(String gameName) { this.gameName = gameName; }
public List<Achievement> getAchievements() { return achievements; }
public void setAchievements(List<Achievement> achievements) { this.achievements = achievements; }
// End Getters/Setters
public void addAchievement(Achievement achievement) { this.achievements.add(achievement); };
}

View file

@ -1,57 +0,0 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
public class Games {
public static class Game {
@JsonProperty("ID")
private int id;
@JsonProperty("name")
private String name;
@JsonProperty("platforms")
private List<String> platforms;
public Game(int id, String name, String platform) {
this.id = id;
this.name = name;
this.platforms = new ArrayList<>();
this.platforms.add(platform);
}
// Start Getters/Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<String> getPlatforms() { return platforms; }
public void setPlatforms(List<String> platforms) { this.platforms = platforms; }
public void addToPlatforms(String platform) { this.platforms.add(platform); }
// End Getters/Setters
}
@JsonProperty("games")
private List<Game> games;
public Games() { games = new ArrayList<Game>(); }
// Start Getters/Setters
public List<Game> getGames() { return games; }
public void setGames(List<Game> games) { this.games = games; }
// End Getters/Setters
public void addGame(Game game) { this.games.add(game); }
}

View file

@ -1,21 +0,0 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public class InternalError {
@JsonProperty
private String message;
public InternalError(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View file

@ -0,0 +1,166 @@
package achievements.data;
import achievements.data.response.search.Achievement;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class Profile {
public static class Platform {
@JsonProperty
private int id;
@JsonProperty("name")
private String name;
@JsonProperty("connected")
private boolean connected;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean getConnected() {
return connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
}
public static class Rating {
@JsonProperty("achievementId")
private int achievementId;
@JsonProperty("name")
private String name;
@JsonProperty("difficulty")
private Float difficulty;
@JsonProperty("quality")
private Float quality;
@JsonProperty("review")
private String review;
public int getAchievementId() {
return achievementId;
}
public void setAchievementId(int achievementId) {
this.achievementId = achievementId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getDifficulty() {
return difficulty;
}
public void setDifficulty(Float difficulty) {
this.difficulty = difficulty;
}
public Float getQuality() {
return quality;
}
public void setQuality(Float quality) {
this.quality = quality;
}
public String getReview() {
return review;
}
public void setReview(String review) {
this.review = review;
}
}
@JsonProperty("username")
private String username;
@JsonProperty("completed")
private int completed;
@JsonProperty("average")
private Integer average;
@JsonProperty("perfect")
private int perfect;
@JsonProperty("noteworthy")
private List<Achievement> noteworthy;
@JsonProperty("platforms")
private List<Platform> platforms;
@JsonProperty("ratings")
private List<Rating> ratings;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getCompleted() {
return completed;
}
public void setCompleted(int completed) {
this.completed = completed;
}
public Integer getAverage() {
return average;
}
public void setAverage(Integer average) {
this.average = average;
}
public int getPerfect() {
return perfect;
}
public void setPerfect(int perfect) {
this.perfect = perfect;
}
public List<Achievement> getNoteworthy() {
return noteworthy;
}
public void setNoteworthy(List<Achievement> noteworthy) {
this.noteworthy = noteworthy;
}
public List<Platform> getPlatforms() {
return platforms;
}
public void setPlatforms(List<Platform> platforms) {
this.platforms = platforms;
}
public List<Rating> getRatings() {
return ratings;
}
public void setRatings(List<Rating> ratings) {
this.ratings = ratings;
}
}

View file

@ -0,0 +1,58 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Session {
@JsonProperty("key")
private String key;
@JsonProperty("id")
private int id;
@JsonProperty("hue")
private int hue;
@JsonProperty("admin")
private boolean admin;
@JsonIgnore
private boolean used;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getHue() {
return hue;
}
public void setHue(int hue) {
this.hue = hue;
}
public boolean isAdmin() {
return admin;
}
public void setAdmin(boolean admin) {
this.admin = admin;
}
public boolean isUsed() {
return used;
}
public void setUsed(boolean used) {
this.used = used;
}
}

View file

@ -0,0 +1,37 @@
package achievements.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
@JsonProperty("email")
public String email;
@JsonProperty("username")
public String username;
@JsonProperty("password")
public String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View file

@ -0,0 +1,37 @@
package achievements.data.importing;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ImportPlatform {
@JsonProperty("userId")
private int userId;
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("name")
private String name;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View file

@ -0,0 +1,38 @@
package achievements.data.importing;
import achievements.data.User;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ImportUser extends User {
@JsonProperty("userId")
private int userId;
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("admin")
private boolean admin;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public boolean isAdmin() {
return admin;
}
public void setAdmin(boolean admin) {
this.admin = admin;
}
}

View file

@ -0,0 +1,28 @@
package achievements.data.importing;
import achievements.data.request.AddPlatform;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ImportUserPlatform extends AddPlatform {
@JsonProperty("userId")
private int userId;
@JsonProperty("userEmail")
private String userEmail;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserEmail() {
return userEmail;
}
public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}
}

View file

@ -0,0 +1,36 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AddPlatform {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("platformId")
private int platformId;
@JsonProperty("platformUserId")
private String platformUserId;
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public int getPlatformId() {
return platformId;
}
public void setPlatformId(int platformId) {
this.platformId = platformId;
}
public String getPlatformUserId() {
return platformUserId;
}
public void setPlatformUserId(String platformUserId) {
this.platformUserId = platformUserId;
}
}

View file

@ -0,0 +1,47 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class RateAchievement {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("difficulty")
private Float difficulty;
@JsonProperty("quality")
private Float quality;
@JsonProperty("review")
private String review;
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public Float getDifficulty() {
return difficulty;
}
public void setDifficulty(Float difficulty) {
this.difficulty = difficulty;
}
public Float getQuality() {
return quality;
}
public void setQuality(Float quality) {
this.quality = quality;
}
public String getReview() {
return review;
}
public void setReview(String review) {
this.review = review;
}
}

View file

@ -0,0 +1,26 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class RemovePlatform {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("platformId")
private int platformId;
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public int getPlatformId() {
return platformId;
}
public void setPlatformId(int platformId) {
this.platformId = platformId;
}
}

View file

@ -0,0 +1,117 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SearchAchievements {
@JsonProperty("searchTerm")
private String searchTerm;
@JsonProperty("userId")
private Integer userId;
@JsonProperty("completed")
private boolean completed;
@JsonProperty("minCompletion")
private Float minCompletion;
@JsonProperty("maxCompletion")
private Float maxCompletion;
@JsonProperty("minDifficulty")
private Float minDifficulty;
@JsonProperty("maxDifficulty")
private Float maxDifficulty;
@JsonProperty("minQuality")
private Float minQuality;
@JsonProperty("maxQuality")
private Float maxQuality;
@JsonProperty("ordering")
private String ordering;
@JsonProperty("orderDirection")
private String orderDirection;
public String getSearchTerm() {
return searchTerm;
}
public void setSearchTerm(String searchTerm) {
this.searchTerm = searchTerm;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
public Float getMinCompletion() {
return minCompletion;
}
public void setMinCompletion(Float minCompletion) {
this.minCompletion = minCompletion;
}
public Float getMaxCompletion() {
return maxCompletion;
}
public void setMaxCompletion(Float maxCompletion) {
this.maxCompletion = maxCompletion;
}
public Float getMinDifficulty() {
return minDifficulty;
}
public void setMinDifficulty(Float minDifficulty) {
this.minDifficulty = minDifficulty;
}
public Float getMaxDifficulty() {
return maxDifficulty;
}
public void setMaxDifficulty(Float maxDifficulty) {
this.maxDifficulty = maxDifficulty;
}
public Float getMinQuality() {
return minQuality;
}
public void setMinQuality(Float minQuality) {
this.minQuality = minQuality;
}
public Float getMaxQuality() {
return maxQuality;
}
public void setMaxQuality(Float maxQuality) {
this.maxQuality = maxQuality;
}
public String getOrdering() {
return ordering;
}
public void setOrdering(String ordering) {
this.ordering = ordering;
}
public String getOrderDirection() {
return orderDirection;
}
public void setOrderDirection(String orderDirection) {
this.orderDirection = orderDirection;
}
}

View file

@ -0,0 +1,117 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SearchGames {
@JsonProperty("searchTerm")
private String searchTerm;
@JsonProperty("userId")
private Integer userId;
@JsonProperty("owned")
private boolean owned;
@JsonProperty("minAvgCompletion")
private Float minAvgCompletion;
@JsonProperty("maxAvgCompletion")
private Float maxAvgCompletion;
@JsonProperty("minNumOwners")
private Float minNumOwners;
@JsonProperty("maxNumOwners")
private Float maxNumOwners;
@JsonProperty("minNumPerfects")
private Float minNumPerfects;
@JsonProperty("maxNumPerfects")
private Float maxNumPerfects;
@JsonProperty("ordering")
private String ordering;
@JsonProperty("orderDirection")
private String orderDirection;
public String getSearchTerm() {
return searchTerm;
}
public void setSearchTerm(String searchTerm) {
this.searchTerm = searchTerm;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public boolean isOwned() {
return owned;
}
public void setOwned(boolean owned) {
this.owned = owned;
}
public Float getMinAvgCompletion() {
return minAvgCompletion;
}
public void setMinAvgCompletion(Float minAvgCompletion) {
this.minAvgCompletion = minAvgCompletion;
}
public Float getMaxAvgCompletion() {
return maxAvgCompletion;
}
public void setMaxAvgCompletion(Float maxAvgCompletion) {
this.maxAvgCompletion = maxAvgCompletion;
}
public Float getMinNumOwners() {
return minNumOwners;
}
public void setMinNumOwners(Float minNumOwners) {
this.minNumOwners = minNumOwners;
}
public Float getMaxNumOwners() {
return maxNumOwners;
}
public void setMaxNumOwners(Float maxNumOwners) {
this.maxNumOwners = maxNumOwners;
}
public Float getMinNumPerfects() {
return minNumPerfects;
}
public void setMinNumPerfects(Float minNumPerfects) {
this.minNumPerfects = minNumPerfects;
}
public Float getMaxNumPerfects() {
return maxNumPerfects;
}
public void setMaxNumPerfects(Float maxNumPerfects) {
this.maxNumPerfects = maxNumPerfects;
}
public String getOrdering() {
return ordering;
}
public void setOrdering(String ordering) {
this.ordering = ordering;
}
public String getOrderDirection() {
return orderDirection;
}
public void setOrderDirection(String orderDirection) {
this.orderDirection = orderDirection;
}
}

View file

@ -0,0 +1,97 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SearchUsers {
@JsonProperty("searchTerm")
private String searchTerm;
@JsonProperty("minOwned")
private Float minOwned;
@JsonProperty("maxOwned")
private Float maxOwned;
@JsonProperty("minCompleted")
private Float minCompleted;
@JsonProperty("maxCompleted")
private Float maxCompleted;
@JsonProperty("minAvgCompletion")
private Float minAvgCompletion;
@JsonProperty("maxAvgCompletion")
private Float maxAvgCompletion;
@JsonProperty("ordering")
private String ordering;
@JsonProperty("orderDirection")
private String orderDirection;
public String getSearchTerm() {
return searchTerm;
}
public void setSearchTerm(String searchTerm) {
this.searchTerm = searchTerm;
}
public Float getMinOwned() {
return minOwned;
}
public void setMinOwned(Float minOwned) {
this.minOwned = minOwned;
}
public Float getMaxOwned() {
return maxOwned;
}
public void setMaxOwned(Float maxOwned) {
this.maxOwned = maxOwned;
}
public Float getMinCompleted() {
return minCompleted;
}
public void setMinCompleted(Float minCompleted) {
this.minCompleted = minCompleted;
}
public Float getMaxCompleted() {
return maxCompleted;
}
public void setMaxCompleted(Float maxCompleted) {
this.maxCompleted = maxCompleted;
}
public Float getMinAvgCompletion() {
return minAvgCompletion;
}
public void setMinAvgCompletion(Float minAvgCompletion) {
this.minAvgCompletion = minAvgCompletion;
}
public Float getMaxAvgCompletion() {
return maxAvgCompletion;
}
public void setMaxAvgCompletion(Float maxAvgCompletion) {
this.maxAvgCompletion = maxAvgCompletion;
}
public String getOrdering() {
return ordering;
}
public void setOrdering(String ordering) {
this.ordering = ordering;
}
public String getOrderDirection() {
return orderDirection;
}
public void setOrderDirection(String orderDirection) {
this.orderDirection = orderDirection;
}
}

View file

@ -0,0 +1,36 @@
package achievements.data.request;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SetUsername {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("userId")
private int userId;
@JsonProperty("username")
private String username;
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View file

@ -0,0 +1,134 @@
package achievements.data.response.search;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class Achievement {
public static class Rating {
@JsonProperty("userId")
private int userId;
@JsonProperty("username")
private String username;
@JsonProperty("difficulty")
private Float difficulty;
@JsonProperty("quality")
private Float quality;
@JsonProperty("review")
private String review;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Float getDifficulty() {
return difficulty;
}
public void setDifficulty(Float difficulty) {
this.difficulty = difficulty;
}
public Float getQuality() {
return quality;
}
public void setQuality(Float quality) {
this.quality = quality;
}
public String getReview() {
return review;
}
public void setReview(String review) {
this.review = review;
}
}
@JsonProperty("ID")
private int ID;
@JsonProperty("game")
private String game;
@JsonProperty("name")
private String name;
@JsonProperty("description")
private String description;
@JsonProperty("completion")
private Integer completion;
@JsonProperty("difficulty")
private Float difficulty;
@JsonProperty("quality")
private Float quality;
@JsonProperty("ratings")
private List<Rating> ratings;
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
public String getGame() {
return game;
}
public void setGame(String game) {
this.game = game;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Integer getCompletion() {
return completion;
}
public void setCompletion(Integer completion) {
this.completion = completion;
}
public Float getDifficulty() {
return difficulty;
}
public void setDifficulty(Float difficulty) {
this.difficulty = difficulty;
}
public Float getQuality() {
return quality;
}
public void setQuality(Float quality) {
this.quality = quality;
}
public List<Rating> getRatings() {
return ratings;
}
public void setRatings(List<Rating> ratings) {
this.ratings = ratings;
}
}

View file

@ -0,0 +1,59 @@
package achievements.data.response.search;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Game {
@JsonProperty("ID")
private int ID;
@JsonProperty("name")
private String name;
@JsonProperty("achievement_count")
private int achievement_count;
@JsonProperty("avg_completion")
private Integer avg_completion;
@JsonProperty("num_owners")
private int num_owners;
@JsonProperty("num_perfects")
private int num_perfects;
public int getID() { return ID; }
public void setID(int ID) { this.ID = ID; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAchievement_count() {
return achievement_count;
}
public void setAchievement_count(int achievement_count) {
this.achievement_count = achievement_count;
}
public Integer getAvg_completion() {
return avg_completion;
}
public void setAvg_completion(Integer avg_completion) {
this.avg_completion = avg_completion;
}
public int getNum_owners() {
return num_owners;
}
public void setNum_owners(int num_owners) {
this.num_owners = num_owners;
}
public int getNum_perfects() {
return num_perfects;
}
public void setNum_perfects(int num_perfects) {
this.num_perfects = num_perfects;
}
}

View file

@ -0,0 +1,67 @@
package achievements.data.response.search;
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
@JsonProperty("ID")
private int ID;
@JsonProperty("username")
private String username;
@JsonProperty("game_count")
private int game_count;
@JsonProperty("achievement_count")
private int achievement_count;
@JsonProperty("avg_completion")
private Integer avg_completion;
@JsonProperty("perfect_games")
private int perfect_games;
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getGame_count() {
return game_count;
}
public void setGame_count(int game_count) {
this.game_count = game_count;
}
public int getAchievement_count() {
return achievement_count;
}
public void setAchievement_count(int achievement_count) {
this.achievement_count = achievement_count;
}
public Integer getAvg_completion() {
return avg_completion;
}
public void setAvg_completion(Integer avg_completion) {
this.avg_completion = avg_completion;
}
public int getPerfect_games() {
return perfect_games;
}
public void setPerfect_games(int perfect_games) {
this.perfect_games = perfect_games;
}
}

View file

@ -0,0 +1,41 @@
package achievements.misc;
import achievements.apis.PlatformAPI;
import achievements.apis.SteamAPI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Component
public class APIList {
@Autowired
private RestTemplate rest;
public final Map<Integer, PlatformAPI> apis = new HashMap<>();
@PostConstruct
private void init() {
/*db = dbs.getConnection();
try {
var stmt = db.prepareCall("{call GetPlatforms()}");
var results = stmt.executeQuery();
while (results.next()) {
var id = results.getInt("ID");
// Wanted to pull some skekery with dynamic class loading and external api jars, but...time is of the essence and I need to cut scope as much as possible
apis.put(id, new ????(id, rest));
}
} catch (Exception e) {
e.printStackTrace();
}*/
apis.put(0, new SteamAPI(0, rest));
}
}

View file

@ -1,56 +1,56 @@
package achievements.services;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.sql.Connection;
import java.sql.SQLException;
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
@Component
public class DbConnectionService {
private Connection connection;
@Value("${database.server}")
private String serverName;
@Value("${database.name}")
private String databaseName;
@Value("${database.user.name}")
private String username;
@Value("${database.user.password}")
private String password;
public DbConnectionService() {}
@PostConstruct
public void connect() {
try {
var dataSource = new SQLServerDataSource();
dataSource.setServerName (serverName );
dataSource.setDatabaseName(databaseName);
dataSource.setUser (username );
dataSource.setPassword (password );
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getConnection() {
return this.connection;
}
@PreDestroy
public void disconnect() {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
package achievements.misc;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.sql.Connection;
import java.sql.SQLException;
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
@Component
public class DbConnection {
private Connection connection;
@Value("${database.server}")
private String serverName;
@Value("${database.name}")
private String databaseName;
@Value("${database.user.name}")
private String username;
@Value("${database.user.password}")
private String password;
public DbConnection() {}
@PostConstruct
public void connect() {
try {
var dataSource = new SQLServerDataSource();
dataSource.setServerName (serverName );
dataSource.setDatabaseName(databaseName);
dataSource.setUser (username );
dataSource.setPassword (password );
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getConnection() {
return this.connection;
}
@PreDestroy
public void disconnect() {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,70 @@
package achievements.misc;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
public class HashManager {
private static final Random RANDOM = new SecureRandom();
public static byte[] hash(byte[] salt, byte[] password) {
try {
var concat = new byte[salt.length + password.length];
int i = 0;
for (; i < salt.length; ++i) {
concat[i] = salt[i];
}
for (int j = 0; j < password.length; ++j) {
concat[i + j] = password[j];
}
var md = MessageDigest.getInstance("SHA-256");
return md.digest(concat);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
public static String encode(byte[] bytes) {
var chars = new char[bytes.length << 1];
for (int i = 0; i < bytes.length; ++i) {
chars[(i << 1) ] = toHex(bytes[i] >> 0);
chars[(i << 1) + 1] = toHex(bytes[i] >> 4);
}
return new String(chars);
}
public static byte[] decode(String data) {
var decoded = new byte[data.length() >> 1];
for (int i = 0; i < data.length(); i += 2) {
int currentByte =
(fromHex(data.charAt(i )) ) |
(fromHex(data.charAt(i + 1)) << 4);
decoded[i >> 1] = (byte) (currentByte & 0xFF);
}
return decoded;
}
public static byte[] generateBytes(int length) {
var bytes = new byte[length];
RANDOM.nextBytes(bytes);
return bytes;
}
public static char toHex(int halfByte) {
halfByte = halfByte & 0xF;
if (0 <= halfByte && halfByte <= 9 ) return (char) (halfByte + '0' );
if (10 <= halfByte && halfByte <= 15) return (char) (halfByte + 'a' - 10);
return '0';
}
public static int fromHex(char c) {
if ('0' <= c && c <= '9') return c - '0';
if ('A' <= c && c <= 'F') return c - 'A' + 10;
if ('a' <= c && c <= 'f') return c - 'a' + 10;
return 0;
}
}

View file

@ -0,0 +1,33 @@
package achievements.misc;
public class Password {
public final String salt;
public final String hash;
private Password(String salt, String hash) {
this.salt = salt;
this.hash = hash;
}
public static Password generate(String password) {
// Generate the salt
var salt = HashManager.generateBytes(16); // 128 bits
return new Password(
HashManager.encode(salt),
HashManager.encode(HashManager.hash(salt, password.getBytes()))
);
}
public static boolean validate(String salt, String password, String hash) {
var srcHash = HashManager.hash(HashManager.decode(salt), password.getBytes());
var targetHash = HashManager.decode(hash);
for (int i = 0; i < srcHash.length; ++i) {
if (srcHash[i] != targetHash[i]) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,73 @@
package achievements.misc;
import achievements.data.Session;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
@Component
public class SessionManager {
private HashMap<String, Session> sessions;
public SessionManager() {
sessions = new HashMap<>();
}
public Session generate(int user, int hue, boolean admin) {
var key = HashManager.encode(HashManager.generateBytes(16));
var session = new Session();
session.setKey(key);
session.setId(user);
session.setHue(hue);
session.setAdmin(admin);
sessions.put(key, session);
return session;
}
public int getUser(String key) {
return sessions.get(key).getId();
}
public void remove(String key) {
sessions.remove(key);
}
public boolean validate(int user, String key) {
var session = sessions.get(key);
return session != null && user == session.getId();
}
public boolean validateAdmin(int user, String key) {
var session = sessions.get(key);
return session != null && user == session.getId() && session.isAdmin();
}
public boolean refresh(String key) {
var foreign = sessions.get(key);
if (foreign != null) {
foreign.setUsed(true);
return true;
} else {
return false;
}
}
// Clean up inactive sessions
@Scheduled(cron = "0 */30 * * * *")
public void clean() {
var remove = new ArrayList<String>();
sessions.forEach((key, session) -> {
if (!session.isUsed()) {
remove.add(session.getKey());
} else {
session.setUsed(false);
}
});
for (var session : remove) {
sessions.remove(session);
}
}
}

View file

@ -0,0 +1,114 @@
package achievements.services;
import achievements.misc.APIList;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileOutputStream;
import java.sql.Connection;
import java.sql.Types;
import java.util.HashSet;
@Service
public class APIService {
@Autowired
private RestTemplate rest;
@Autowired
private APIList apis;
@Autowired
private DbConnection dbs;
private Connection db;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
private String getFileType(String imagePath) {
var path = imagePath.split("\\.");
return path[path.length - 1];
}
public int importUserPlatform(int userId, int platformId, String platformUserId) {
try {
var response = apis.apis.get(platformId).get(platformUserId);
var addIfNotGame = db.prepareCall("{call AddIfNotGame(?, ?, ?)}");
var addGameToPlatform = db.prepareCall("{call AddGameToPlatform(?, ?, ?)}");
var addGameToUser = db.prepareCall("{call AddGameToUser(?, ?, ?)}");
var addIfNotAchievement = db.prepareCall("{call AddIfNotAchievement(?, ?, ?, ?, ?, ?)}");
var setAchievementProgressForUser = db.prepareCall("{call SetAchievementProgressForUser(?, ?, ?, ?)}");
addIfNotGame.registerOutParameter(3, Types.INTEGER);
addIfNotAchievement.registerOutParameter(6, Types.INTEGER);
for (var game : response.getGames()) {
addIfNotGame.setString(1, game.getName());
addIfNotGame.setString(2, getFileType(game.getThumbnail()));
addIfNotGame.execute();
var gameId = addIfNotGame.getInt(3);
addGameToPlatform.setInt(1, gameId);
addGameToPlatform.setInt(2, platformId);
addGameToPlatform.setString(3, game.getPlatformGameId());
addGameToPlatform.execute();
var gameThumbnail = new File("storage/images/game/" + gameId + "." + getFileType(game.getThumbnail()));
if (!gameThumbnail.exists()) {
var bytes = rest.getForObject(game.getThumbnail(), byte[].class);
var stream = new FileOutputStream(gameThumbnail);
stream.write(bytes);
stream.close();
}
addGameToUser.setInt(1, gameId);
addGameToUser.setInt(2, userId);
addGameToUser.setInt(3, platformId);
addGameToUser.execute();
var set = new HashSet<Integer>();
for (var achievement : game.getAchievements()) {
addIfNotAchievement.setInt(1, gameId);
addIfNotAchievement.setString(2, achievement.getName());
addIfNotAchievement.setString(3, achievement.getDescription());
addIfNotAchievement.setInt(4, achievement.getStages());
addIfNotAchievement.setString(5, getFileType(achievement.getThumbnail()));
addIfNotAchievement.execute();
var achievementId = addIfNotAchievement.getInt(6);
set.add(achievementId);
var achievementIcon = new File("storage/images/achievement/" + achievementId + "." + getFileType(achievement.getThumbnail()));
if (!achievementIcon.exists()) {
var bytes = rest.getForObject(achievement.getThumbnail(), byte[].class);
var stream = new FileOutputStream(achievementIcon);
stream.write(bytes);
stream.close();
}
if (game.isPlayed()) {
setAchievementProgressForUser.setInt(1, userId);
setAchievementProgressForUser.setInt(2, platformId);
setAchievementProgressForUser.setInt(3, achievementId);
setAchievementProgressForUser.setInt(4, achievement.getProgress());
setAchievementProgressForUser.execute();
}
}
}
addIfNotGame.close();
addGameToPlatform.close();
addIfNotAchievement.close();
setAchievementProgressForUser.close();
return 0;
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
}

View file

@ -0,0 +1,146 @@
package achievements.services;
import achievements.data.request.RateAchievement;
import achievements.data.response.search.Achievement;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.Connection;
import java.sql.Types;
import java.util.ArrayList;
@Service
public class AchievementService {
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private ImageService imageService;
@Autowired
private AuthenticationService authService;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
public String[] getIcon(int achievementId) {
try {
var stmt = db.prepareCall("{call GetAchievementIcon(?)}");
return imageService.getImageType(stmt, achievementId);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Achievement getAchievement(int achievementId) {
try {
var stmt = db.prepareCall("{call GetAchievement(?)}");
stmt.setInt(1, achievementId);
var result = stmt.executeQuery();
if (result.next()) {
var achievement = new Achievement();
achievement.setID(result.getInt("ID"));
achievement.setName(result.getString("Name"));
achievement.setCompletion(result.getInt("Completion")); if (result.wasNull()) { achievement.setCompletion(null); }
achievement.setDescription(result.getString("Description"));
achievement.setDifficulty(result.getFloat("Difficulty")); if (result.wasNull()) { achievement.setDifficulty(null); }
achievement.setQuality(result.getFloat("Quality")); if (result.wasNull()) { achievement.setQuality(null); }
stmt = db.prepareCall("{call GetRatingsForAchievement(?)}");
stmt.setInt(1, achievementId);
var ratings = new ArrayList<Achievement.Rating>();
var results = stmt.executeQuery();
while (results.next()) {
var rating = new Achievement.Rating();
rating.setUserId(results.getInt("UserID"));
rating.setUsername(results.getString("Username"));
rating.setDifficulty(results.getFloat("Difficulty")); if (results.wasNull()) { rating.setDifficulty(null); }
rating.setQuality(results.getFloat("Quality")); if (results.wasNull()) { rating.setQuality(null); }
rating.setReview(results.getString("Description"));
ratings.add(rating);
}
achievement.setRatings(ratings);
return achievement;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public RateAchievement getRating(int achievement, int user) {
try {
var stmt = db.prepareCall("{call HasProgress(?, ?, ?)}");
stmt.setInt(1, user);
stmt.setInt(2, achievement);
stmt.registerOutParameter(3, Types.BOOLEAN);
stmt.execute();
if (stmt.getBoolean(3)) {
stmt = db.prepareCall("{call GetRating(?, ?)}");
stmt.setInt(1, user);
stmt.setInt(2, achievement);
var result = stmt.executeQuery();
if (result.next()) {
var rating = new RateAchievement();
rating.setDifficulty(result.getFloat("Difficulty")); if (result.wasNull()) { rating.setDifficulty(null); }
rating.setQuality(result.getFloat("Quality")); if (result.wasNull()) { rating.setQuality(null); }
rating.setReview(result.getString("Description"));
return rating;
} else {
return new RateAchievement();
}
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public RateAchievement setRating(int achievementId, int userId, RateAchievement rateAchievement) {
if (authService.session().validate(userId, rateAchievement.getSessionKey())) {
try {
var stmt = db.prepareCall("{call SetRating(?, ?, ?, ?, ?)}");
stmt.setInt(1, userId);
stmt.setInt(2, achievementId);
stmt.setFloat(3, rateAchievement.getDifficulty());
stmt.setFloat(4, rateAchievement.getQuality());
stmt.setString(5, rateAchievement.getReview());
stmt.execute();
return rateAchievement;
} catch (Exception e) {
e.printStackTrace();
}
}
try {
var stmt = db.prepareCall("{call GetRating(?, ?)}");
stmt.setInt(1, userId);
stmt.setInt(2, achievementId);
var result = stmt.executeQuery();
if (result.next()) {
var rating = new RateAchievement();
rating.setDifficulty(result.getFloat("Difficulty"));
rating.setQuality(result.getFloat("Quality"));
rating.setReview(result.getString("Review"));
return rating;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -0,0 +1,141 @@
package achievements.services;
import achievements.data.Session;
import achievements.data.User;
import achievements.misc.DbConnection;
import achievements.misc.Password;
import achievements.misc.SessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.*;
@Service
public class AuthenticationService {
public static class LoginResponse {
public int status;
public Session session;
public LoginResponse() {
this.status = 0;
}
public LoginResponse(int status) {
this.status = status;
this.session = null;
}
public LoginResponse(int status, Session session) {
this.status = status;
this.session = session;
}
}
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private SessionManager session;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
public LoginResponse createUser(User user) {
if (!user.getEmail().matches(".+@\\w+\\.\\w+")) {
return new LoginResponse(2);
}
try {
var statement = db.prepareCall("{? = call CreateUser(?, ?, ?, ?, ?, ?)}");
statement.registerOutParameter(1, Types.INTEGER);
statement.setString(2, user.getEmail());
statement.setString(3, user.getUsername());
var password = Password.generate(user.getPassword());
statement.setString(4, password.salt);
statement.setString(5, password.hash);
statement.registerOutParameter(6, Types.INTEGER);
statement.registerOutParameter(7, Types.INTEGER);
statement.execute();
var response = new LoginResponse(
statement.getInt(1),
session.generate(
statement.getInt(6),
statement.getInt(7),
false
)
);
statement.close();
return response;
} catch (SQLException e) {
e.printStackTrace();
}
return new LoginResponse(-1);
}
public LoginResponse login(User user) {
var response = new LoginResponse(-1);
try {
var statement = db.prepareCall("{? = call GetUserLogin(?)}");
statement.registerOutParameter(1, Types.INTEGER);
statement.setString(2, user.email);
statement.execute();
if (statement.getInt(1) == 0) {
var result = statement.executeQuery();
result.next();
var salt = result.getString("Salt");
var hash = result.getString("Password");
if (Password.validate(salt, user.getPassword(), hash)) {
response = new LoginResponse(
0,
session.generate(
result.getInt("ID"),
result.getInt("Hue"),
result.getBoolean("Admin")
)
);
} else {
response = new LoginResponse(2);
}
} else {
response = new LoginResponse(1);
}
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
return response;
}
public boolean refresh(Session key) { return session.refresh(key.getKey()); }
public boolean openAuth() {
try {
var stmt = db.prepareCall("{call HasUser(?)}");
stmt.registerOutParameter(1, Types.BOOLEAN);
stmt.execute();
return !stmt.getBoolean(1);
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public void logout(Session key) {
session.remove(key.getKey());
}
public SessionManager session() {
return session;
}
}

View file

@ -1,87 +0,0 @@
package achievements.services;
import achievements.data.Achievements;
import achievements.data.Games;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.*;
@Service
public class DbService {
@Autowired
private DbConnectionService dbs;
private Connection db;
@PostConstruct
private void init() { db = dbs.getConnection(); }
public Achievements getAchievements(String gameName) {
try {
// Create Query
CallableStatement stmt = db.prepareCall("{? = call GetAchievements(?)}");
stmt.registerOutParameter(1, Types.INTEGER);
stmt.setString(2, gameName);
// Read Result(s)
ResultSet results = stmt.executeQuery();
var achievements = new Achievements();
while (results.next()) {
// Add Result(s) to data class
int achievementGameID = results.getInt("GameID");
String achievementGameName = results.getString("GameName");
String achievementName = results.getString("Name");
String achievementDescription = results.getString("Description");
int achievementStages = results.getInt("Stages");
// Checks if getting from specific game or all achievements
if (!gameName.equals("%")) {
achievements.setGameID(achievementGameID);
achievements.setGameName(achievementGameName);
}
achievements.addAchievement(new Achievements.Achievement(achievementName, achievementDescription, achievementStages));
}
stmt.close();
return achievements;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public Games getGames(String name) {
try {
// Create Query
CallableStatement stmt = db.prepareCall("{? = call GetGame(?)}");
stmt.registerOutParameter(1, Types.INTEGER);
stmt.setString(2, name);
// Read Result(s)
ResultSet results = stmt.executeQuery();
var games = new Games();
while (results.next()) {
// Add Result(s) to data class
int gameID = results.getInt("ID");
String gameName = results.getString("Name");
String gamePlatform = results.getString("PlatformName");
if (!games.getGames().isEmpty()) {
var lastGame = games.getGames().get(games.getGames().size()-1);
if (lastGame.getId() == gameID) {
lastGame.addToPlatforms(gamePlatform);
} else {
games.addGame(new Games.Game(gameID,gameName,gamePlatform));
}
} else {
games.addGame(new Games.Game(gameID,gameName,gamePlatform));
}
}
stmt.close();
return games;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
}

View file

@ -0,0 +1,34 @@
package achievements.services;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.Connection;
@Service
public class GameService {
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private ImageService imageService;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
public String[] getIcon(int gameId) {
try {
var stmt = db.prepareCall("{call GetGameIcon(?)}");
return imageService.getImageType(stmt, gameId);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -0,0 +1,80 @@
package achievements.services;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.util.HashMap;
@Service
public class ImageService {
public static final HashMap<String, String> MIME_TO_EXT = new HashMap<>();
public static final HashMap<String, String> EXT_TO_MIME = new HashMap<>();
static {
MIME_TO_EXT.put("apng", "apng");
MIME_TO_EXT.put("avif", "avif");
MIME_TO_EXT.put("gif", "gif" );
MIME_TO_EXT.put("jpeg", "jpg" );
MIME_TO_EXT.put("png", "png" );
MIME_TO_EXT.put("svg+xml", "svg" );
MIME_TO_EXT.put("webp", "webp");
EXT_TO_MIME.put("apng", "apng" );
EXT_TO_MIME.put("avif", "avif" );
EXT_TO_MIME.put("gif", "gif" );
EXT_TO_MIME.put("jpg", "jpeg" );
EXT_TO_MIME.put("png", "png" );
EXT_TO_MIME.put("svg", "svg+xml");
EXT_TO_MIME.put("webp", "webp" );
}
public String[] getImageType(CallableStatement stmt, int id) {
try {
stmt.setInt(1, id);
var result = stmt.executeQuery();
if (result.next()) {
var type = result.getString(1);
if (type != null) {
return new String[] { id + "." + type, EXT_TO_MIME.get(type) };
}
}
} catch (SQLException e) {
e.printStackTrace();
} catch (NumberFormatException e) {
e.printStackTrace();
}
return null;
}
public void send(String[] image, String type, HttpServletResponse response) {
var file = (File) null;
var mimeType = (String) null;
if (image == null) {
file = new File("storage/images/default/" + type + ".png");
mimeType = "png";
} else {
file = new File("storage/images/" + type + "/" + image[0]);
mimeType = image[1];
}
try {
var stream = new FileInputStream(file);
IOUtils.copy(stream, response.getOutputStream());
response.setStatus(200);
response.setContentType("image/" + mimeType);
response.flushBuffer();
stream.close();
return;
} catch (IOException e) {
e.printStackTrace();
}
response.setStatus(500);
}
}

View file

@ -0,0 +1,81 @@
package achievements.services;
import achievements.data.importing.ImportPlatform;
import achievements.data.importing.ImportUser;
import achievements.data.importing.ImportUserPlatform;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
@Service
public class ImportService {
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private AuthenticationService authService;
@Autowired
private UserService userService;
@PostConstruct
public void init() {
db = dbs.getConnection();
}
public int importPlatform(ImportPlatform platform) {
if (authService.session().validateAdmin(platform.getUserId(), platform.getSessionKey())) {
try {
var stmt = db.prepareCall("{call AddPlatform(?, ?)}");
stmt.setString(1, platform.getName());
stmt.registerOutParameter(2, Types.INTEGER);
stmt.execute();
return 0;
} catch (SQLException e) {
e.printStackTrace();
}
}
return -1;
}
public int importUser(ImportUser user) {
if (authService.session().validateAdmin(user.getUserId(), user.getSessionKey())) {
try {
var response = authService.createUser(user);
if (user.isAdmin()) {
var stmt = db.prepareCall("{call OpUser(?)}");
stmt.setInt(1, response.session.getId());
stmt.execute();
}
return 0;
} catch (SQLException e) {
e.printStackTrace();
}
}
return -1;
}
public int importUserPlatform(ImportUserPlatform userPlatform) {
if (authService.session().validateAdmin(userPlatform.getUserId(), userPlatform.getSessionKey())) {
try {
var stmt = db.prepareCall("{call GetIdFromEmail(?, ?)}");
stmt.setString(1, userPlatform.getUserEmail());
stmt.registerOutParameter(2, Types.INTEGER);
stmt.execute();
return userService.addPlatform(stmt.getInt(2), userPlatform, false);
} catch (Exception e) {
e.printStackTrace();
}
}
return -1;
}
}

View file

@ -0,0 +1,34 @@
package achievements.services;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.Connection;
@Service
public class PlatformService {
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private ImageService imageService;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
public String[] getIcon(int platformId) {
try {
var stmt = db.prepareCall("{call GetPlatformIcon(?)}");
return imageService.getImageType(stmt, platformId);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -0,0 +1,132 @@
package achievements.services;
import achievements.data.request.SearchGames;
import achievements.data.request.SearchUsers;
import achievements.data.response.search.Achievement;
import achievements.data.request.SearchAchievements;
import achievements.data.response.search.Game;
import achievements.data.response.search.User;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
@Service
public class SearchService {
@Autowired
private DbConnection dbs;
private Connection db;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
public List<Achievement> searchAchievements(SearchAchievements query) {
try {
var stmt = db.prepareCall("{call SearchAchievements(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}");
stmt.setString(1, query.getSearchTerm());
stmt.setBoolean(3, query.isCompleted());
if (query.getUserId() != null) { stmt.setInt (2, query.getUserId()); } else { stmt.setString(2, null); }
if (query.getMinCompletion() != null) { stmt.setFloat(4, query.getMinCompletion()); } else { stmt.setString(4, null); }
if (query.getMaxCompletion() != null) { stmt.setFloat(5, query.getMaxCompletion()); } else { stmt.setString(5, null); }
if (query.getMinDifficulty() != null) { stmt.setFloat(6, query.getMinDifficulty()); } else { stmt.setString(6, null); }
if (query.getMaxDifficulty() != null) { stmt.setFloat(7, query.getMaxDifficulty()); } else { stmt.setString(7, null); }
if (query.getMinQuality() != null) { stmt.setFloat(8, query.getMinQuality()); } else { stmt.setString(8, null); }
if (query.getMaxQuality() != null) { stmt.setFloat(9, query.getMaxQuality()); } else { stmt.setString(9, null); }
stmt.setString(10, query.getOrdering());
stmt.setString(11, query.getOrderDirection());
var results = stmt.executeQuery();
var achievements = new ArrayList<Achievement>();
while (results.next()) {
var achievement = new Achievement();
achievement.setID (results.getInt ("ID" ));
achievement.setGame (results.getString("Game" ));
achievement.setName (results.getString("Name" ));
achievement.setCompletion(results.getInt ("Completion")); if (results.wasNull()) { achievement.setCompletion(null); }
achievement.setDifficulty(results.getFloat ("Difficulty")); if (results.wasNull()) { achievement.setDifficulty(null); }
achievement.setQuality (results.getFloat ("Quality" )); if (results.wasNull()) { achievement.setQuality (null); }
achievements.add(achievement);
}
return achievements;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public List<User> searchUsers(SearchUsers query) {
try {
var stmt = db.prepareCall("{call SearchUsers(?, ?, ?, ?, ?, ?, ?, ?, ?)}");
stmt.setString(1, query.getSearchTerm());
if (query.getMinOwned() != null) { stmt.setFloat(2, query.getMinOwned()); } else { stmt.setString(2, null); }
if (query.getMaxOwned() != null) { stmt.setFloat(3, query.getMaxOwned()); } else { stmt.setString(3, null); }
if (query.getMinCompleted() != null) { stmt.setFloat(4, query.getMinCompleted()); } else { stmt.setString(4, null); }
if (query.getMaxCompleted() != null) { stmt.setFloat(5, query.getMaxCompleted()); } else { stmt.setString(5, null); }
if (query.getMinAvgCompletion() != null) { stmt.setFloat(6, query.getMinAvgCompletion()); } else { stmt.setString(6, null); }
if (query.getMaxAvgCompletion() != null) { stmt.setFloat(7, query.getMaxAvgCompletion()); } else { stmt.setString(7, null); }
stmt.setString(8, query.getOrdering());
stmt.setString(9, query.getOrderDirection());
var results = stmt.executeQuery();
var users = new ArrayList<User>();
while (results.next()) {
var user = new User();
user.setID (results.getInt ("ID" ));
user.setUsername (results.getString("Username" ));
user.setGame_count (results.getInt ("GameCount" ));
user.setAchievement_count(results.getInt ("AchievementCount"));
user.setAvg_completion (results.getInt ("AvgCompletion" )); if (results.wasNull()) { user.setAvg_completion(null); }
user.setPerfect_games (results.getInt ("PerfectGames" ));
users.add(user);
}
return users;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public List<Game> searchGames(SearchGames query) {
try {
var stmt = db.prepareCall("{call SearchGames(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}");
stmt.setString(1, query.getSearchTerm());
stmt.setBoolean(3, query.isOwned());
if (query.getUserId() != null) { stmt.setInt (2, query.getUserId()); } else { stmt.setString(2, null); }
if (query.getMinAvgCompletion() != null) { stmt.setFloat(4, query.getMinAvgCompletion()); } else { stmt.setString(4, null); }
if (query.getMaxAvgCompletion() != null) { stmt.setFloat(5, query.getMaxAvgCompletion()); } else { stmt.setString(5, null); }
if (query.getMinNumOwners() != null) { stmt.setFloat(6, query.getMinNumOwners()); } else { stmt.setString(6, null); }
if (query.getMaxNumOwners() != null) { stmt.setFloat(7, query.getMaxNumOwners()); } else { stmt.setString(7, null); }
if (query.getMinNumPerfects() != null) { stmt.setFloat(8, query.getMinNumPerfects()); } else { stmt.setString(8, null); }
if (query.getMaxNumPerfects() != null) { stmt.setFloat(9, query.getMaxNumPerfects()); } else { stmt.setString(9, null); }
stmt.setString(10, query.getOrdering());
stmt.setString(11, query.getOrderDirection());
var results = stmt.executeQuery();
var games = new ArrayList<Game>();
while (results.next()) {
var game = new Game();
game.setID (results.getInt ("ID" ));
game.setName (results.getString("Name" ));
game.setAchievement_count(results.getInt ("AchievementCount"));
game.setAvg_completion (results.getInt ("AvgCompletion" )); if (results.wasNull()) { game.setAvg_completion(null); }
game.setNum_owners (results.getInt ("NumOwners" ));
game.setNum_perfects (results.getInt ("NumPerfects" ));
games.add(game);
}
return games;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -0,0 +1,256 @@
package achievements.services;
import achievements.data.Profile;
import achievements.data.request.AddPlatform;
import achievements.data.request.RemovePlatform;
import achievements.data.request.SetUsername;
import achievements.data.response.search.Achievement;
import achievements.misc.DbConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileOutputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import static achievements.services.ImageService.MIME_TO_EXT;
@Service
public class UserService {
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private AuthenticationService auth;
@Autowired
private APIService apiService;
@Autowired
private ImageService imageService;
@PostConstruct
private void init() {
db = dbs.getConnection();
}
public Profile getProfile(int userId) {
try {
var profile = (Profile) null;
{
var stmt = db.prepareCall("{? = call GetUserNameAndStats(?, ?, ?, ?, ?)}");
stmt.registerOutParameter(1, Types.INTEGER);
stmt.setInt(2, userId);
stmt.registerOutParameter(3, Types.VARCHAR);
stmt.registerOutParameter(4, Types.INTEGER);
stmt.registerOutParameter(5, Types.INTEGER);
stmt.registerOutParameter(6, Types.INTEGER);
stmt.execute();
if (stmt.getInt(1) == 0) {
profile = new Profile();
profile.setUsername(stmt.getString(3));
profile.setCompleted(stmt.getInt(4));
var average = stmt.getString(5);
profile.setPerfect(stmt.getInt(6));
if (average != null) {
profile.setAverage(Integer.parseInt(average));
}
} else {
return null;
}
}
{
var stmt = db.prepareCall("{call GetUserPlatforms(?)}");
stmt.setInt(1, userId);
var results = stmt.executeQuery();
var platforms = new ArrayList<Profile.Platform>();
while (results.next()) {
var platform = new Profile.Platform();
platform.setId (results.getInt ("ID" ));
platform.setName (results.getString ("PlatformName"));
platform.setConnected(results.getBoolean("Connected" ));
platforms.add(platform);
}
profile.setPlatforms(platforms);
}
{
var stmt = db.prepareCall("{call GetRatingsByUser(?)}");
stmt.setInt(1, userId);
var results = stmt.executeQuery();
var ratings = new ArrayList<Profile.Rating>();
while (results.next()) {
var rating = new Profile.Rating();
rating.setAchievementId(results.getInt("AchievementID"));
rating.setName(results.getString("Name"));
rating.setDifficulty(results.getFloat("Difficulty")); if (results.wasNull()) { rating.setDifficulty(null); }
rating.setQuality(results.getFloat("Quality")); if (results.wasNull()) { rating.setQuality(null); }
rating.setReview(results.getString("Description"));
ratings.add(rating);
}
profile.setRatings(ratings);
}
return profile;
} catch (SQLException e) {
e.printStackTrace();
} catch (NumberFormatException e) {
e.printStackTrace();
}
return null;
}
public int setUsername(int userId, SetUsername username) {
try {
if (auth.session().validate(userId, username.getSessionKey()) && username.getUsername().length() > 0 && username.getUsername().length() <= 32) {
var stmt = db.prepareCall("{call SetUsername(?, ?)}");
stmt.setInt(1, userId);
stmt.setString(2, username.getUsername());
stmt.execute();
return 0;
}
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
public String[] getProfileImage(int userId) {
try {
var stmt = db.prepareCall("{call GetUserImage(?)}");
return imageService.getImageType(stmt, userId);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String setProfileImage(int userId, String sessionKey, MultipartFile file) {
try {
var type = file.getContentType();
if (type.matches("image/.*")) {
type = type.substring(6);
type = MIME_TO_EXT.get(type);
if (!auth.session().validate(userId, sessionKey)) {
return "forbidden";
} else if (type == null) {
return "unsupported_type";
} else {
var stmt = db.prepareCall("{call SetUserImage(?, ?, ?)}");
stmt.setInt(1, userId);
stmt.setString(2, type);
stmt.registerOutParameter(3, Types.VARCHAR);
stmt.execute();
var oldType = stmt.getString(3);
// Delete old file
if (oldType != null && type != oldType) {
var oldFile = new File("storage/images/user/" + userId + "." + oldType);
if (oldFile.exists()) {
oldFile.delete();
}
}
// Save new file (will overwrite old if file type didn't change)
{
var image = new FileOutputStream("storage/images/user/" + userId + "." + type);
FileCopyUtils.copy(file.getInputStream(), image);
image.close();
}
return "success";
}
} else {
return "not_an_image";
}
} catch (Exception e) {
e.printStackTrace();
}
return "unknown";
}
public int addPlatform(int userId, AddPlatform request, boolean validate) {
if (!validate || auth.session().validate(userId, request.getSessionKey())) {
try {
db.setAutoCommit(false);
try {
var stmt = db.prepareCall("{call AddUserToPlatform(?, ?, ?)}");
stmt.setInt(1, userId);
stmt.setInt(2, request.getPlatformId());
stmt.setString(3, request.getPlatformUserId());
stmt.execute();
int successful = apiService.importUserPlatform(userId, request.getPlatformId(), request.getPlatformUserId());
if (successful == 0) {
db.commit();
db.setAutoCommit(true);
return 0;
}
} catch (Exception e) {
e.printStackTrace();
}
db.rollback();
db.setAutoCommit(true);
} catch(SQLException e){
e.printStackTrace();
}
}
return -1;
}
public int removePlatform(int userId, RemovePlatform request) {
try {
if (auth.session().validate(userId, request.getSessionKey())) {
var stmt = db.prepareCall("{call RemoveUserFromPlatform(?, ?)}");
stmt.setInt(1, userId);
stmt.setInt(2, request.getPlatformId());
stmt.execute();
return 0;
}
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
public List<Achievement> getNoteworthy(int userId) {
try {
var stmt = db.prepareCall("{call GetNoteworthyAchievementsForUser(?)}");
stmt.setInt(1, userId);
var results = stmt.executeQuery();
var achievements = new ArrayList<Achievement>();
while (results.next()) {
var achievement = new Achievement();
achievement.setID(results.getInt("ID"));
achievement.setName(results.getString("Name"));
achievement.setCompletion(results.getInt("Completion"));
achievements.add(achievement);
}
return achievements;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -1,2 +1,8 @@
server.port = 4730
spring.application.name = Achievements Project
spring.jackson.default-property-inclusion=always
server.session.cookie.secure = false
spring.servlet.multipart.max-file-size = 10MB
spring.servlet.multipart.max-request-size = 10MB

3
frontend/.gitignore vendored
View file

@ -1,3 +1,6 @@
# Node files
node_modules/
package-lock.json
# Import Data
import.json

View file

@ -1 +1,5 @@
{}
{
"hosts": {
"backend": "https://localhost:4730"
}
}

View file

@ -2,6 +2,9 @@
"extends": [
"config/base.json"
],
"hosts": {
"frontend": "http://localhost:8080"
},
"build": "debug",
"port": 8080
}

View file

@ -2,6 +2,9 @@
"extends": [
"config/base.json"
],
"hosts": {
"frontend": "http://localhost"
},
"build": "release",
"port": 80
}

View file

@ -3,17 +3,19 @@
"version": "1.0.0",
"description": "Cross platform achievement tracker",
"repository": "github:Gnarwhal/AchievementProject",
"main": "static_server.js",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"debug": "node static_server.js config/debug.json",
"release": "node static_server.js config/release.json"
"debug": "node server.js config/debug.json",
"release": "node server.js config/release.json"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"morgan": "^1.10.0",
"xml2js": "^0.4.23"
"passport": "^0.4.1",
"passport-steam": "^1.0.15",
"promptly": "^3.2.0"
}
}

54
frontend/server.js Normal file
View file

@ -0,0 +1,54 @@
const fs = require('fs' );
const path = require('path' );
const https = require('https' );
const express = require('express' );
const morgan = require('morgan' );
const passport = require('passport');
const SteamStrategy = require('passport-steam').Strategy;
const promptly = require('promptly');
const config = require('./config.js').load(process.argv[2]);
console.log(`Running server at '${config.hosts.frontend}'`);
passport.use(new SteamStrategy({
returnURL: `${config.hosts.frontend}/user/steam`,
realm: `${config.hosts.frontend}`,
profile: false,
}));
const app = express();
app.use("/", morgan("dev"));
app.use("/static", express.static("webpage/static"));
app.get("/login", (req, res) => res.sendFile(path.join(__dirname + "/webpage/login.html")));
app.get("/", (req, res) => res.sendFile(path.join(__dirname + "/webpage/search_achievements.html")));
app.get("/achievements", (req, res) => res.sendFile(path.join(__dirname + "/webpage/search_achievements.html")));
app.get("/users", (req, res) => res.sendFile(path.join(__dirname + "/webpage/search_users.html")));
app.get("/games", (req, res) => res.sendFile(path.join(__dirname + "/webpage/search_games.html")));
app.get("/import", (req, res) => res.sendFile(path.join(__dirname + "/webpage/import.html")));
app.get("/achievement/:id", (req, res) => res.sendFile(path.join(__dirname + "/webpage/achievement.html")));
app.get("/user/:id", (req, res) => res.sendFile(path.join(__dirname + "/webpage/user.html")));
app.get("/auth/steam", passport.authenticate('steam'), (req, res) => {});
// --- API Forward --- //
app.use("/api/*", (req, res) => {
res.redirect(307, `${config.hosts.backend}/${req.params[0]}`)
});
// ------------------- //
const server = app.listen(config.port);
const prompt = input => {
if (/q(?:uit)?|exit/i.test(input)) {
server.close();
} else {
promptly.prompt('')
.then(prompt);
}
};
prompt();

View file

@ -1,16 +0,0 @@
const express = require('express');
const morgan = require('morgan' );
const fs = require('fs' );
const https = require('https' );
const config = require('./config.js').load(process.argv[2]);
if (config.build === 'debug') {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
}
const app = express();
app.use("/", morgan("dev"));
app.use("/", express.static("webpage"));
app.listen(config.port);

View file

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/achievement.css" />
</head>
<body>
<div id="navbar">
<template data-template="navbar: List<Basic>">
<div id="navbar-section-${section}" class="navbar-section">
<template data-template="navbar-section-${section}: List<Basic>">
<div id="navbar-item-${item}" class="navbar-item" data-page-name="${item}">
${title}
</div>
</template>
</div>
</template>
</div>
<div id="content-body">
<div id="achievement-page" class="page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Achievement</p>
<div class="page-header-separator"></div>
</div>
</div>
<div id="importing">
<p id="importing-text">Contemplating...</p>
<img id="importing-loading" class="ap-loading" src="/static/res/loading.svg" alt="Loading Symbol" />
</div>
<template data-template="achievement-page">
<div id="achievement-section-0">
<div id="achievement-info" class="page-subsection">
<div class="page-subsection-wrapper">
<div id="achievement-info-subheader" class="page-subheader">
<div id="achievement-info-flex" class="page-subheader-flex">
<p id="achievement-name-text" class="page-subheader-text">${name}</p>
<img id="achievement-icon-img" class="lazy-img" data-src="/api/achievement/${id}/image" alt="Achievement Icon" />
</div>
<div class="page-subheader-separator"></div>
</div>
<p id="achievement-description-text" class="page-subsection-chunk ap-text">${description}</p>
</div>
</div>
</div>
<div id="achievement-section-1">
<div id="achievement-stats" class="page-subsection">
<div id="achievement-stats-numeric">
<div id="achievement-completion" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Completion Rate</p>
<div class="page-subheader-separator"></div>
</div>
<div id="achievement-completion-stack">
<img id="achievement-completion-background" src="/static/res/completion.svg">
<canvas id="achievement-completion-canvas"></canvas>
<p id="achievement-completion-text">${completion}</p>
</div>
</div>
<div id="achievement-difficulty" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Difficulty</p>
<div class="page-subheader-separator"></div>
</div>
<p id="achievement-difficulty-text">${difficulty}</p>
</div>
<div id="achievement-quality" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Quality</p>
<div class="page-subheader-separator"></div>
</div>
<p id="achievement-quality-text">${quality}</p>
</div>
</div>
</div>
<div id="achievement-rating" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<div class="page-subheader-flex">
<p class="page-subheader-text">My Rating</p>
<span id="rating-save-stack" class="achievement-save-stack">
<img class="achievement-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
<img class="achievement-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Save Platforms Hovered" />
</span>
</div>
<div class="page-subheader-separator"></div>
</div>
<div id="achievement-rating-subsection" class="page-subsection-chunk">
<div id="achievement-rating-numeric">
<div id="achievement-difficulty-rating" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Difficulty</p>
<div class="page-subheader-separator"></div>
</div>
<div class="achievement-rating-text-flex">
<input type="text" id="achievement-difficulty-rating-text" class="achievement-rating-text" value="${my_difficulty}"></input>
<p class="achievement-rating-max-text">/ 10</p>
</div>
</div>
<div id="achievement-quality-rating" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Quality</p>
<div class="page-subheader-separator"></div>
</div>
<div class="achievement-rating-text-flex">
<input type="text" id="achievement-quality-rating-text" class="achievement-rating-text" value="${my_quality}"></input>
<p class="achievement-rating-max-text">/ 10</p>
</div>
</div>
</div>
<div id="achievement-description-rating" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Review</p>
<div class="page-subheader-separator"></div>
</div>
<textarea id="achievement-review-rating-text">${my_review}</textarea>
</div>
</div>
</div>
</div>
</div>
<div id="achievement-section-2">
<div id="achievement-ratings" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Ratings</p>
<div class="page-subheader-separator"></div>
</div>
<div class="page-subsection-chunk">
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon"></p>
<p class="list-page-entry-text rating-username">Username</p>
<p class="list-page-entry-text rating-difficulty">Difficulty</p>
<p class="list-page-entry-text rating-quality">Quality</p>
<p class="list-page-entry-text rating-review">Review</p>
</div>
<template data-template="rating-list: List<Basic>">
<div class="list-page-entry rating" data-id="${user_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/user/${user_id}/image" alt="User Image"></img>
<p class="list-page-entry-text rating-username">${user_username}</p>
<p class="list-page-entry-text rating-difficulty">${user_difficulty}</p>
<p class="list-page-entry-text rating-quality">${user_quality}</p>
<p class="list-page-entry-text rating-review">${user_review}</p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/achievement.js"></script>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project | Import</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/import.css" />
</head>
<body>
<div id="navbar"></div>
<div id="content-body">
<div id="import-page" class="page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Import</p>
<div class="page-header-separator"></div>
</div>
</div>
<div id="import-dropzone" class="page-subsection">
<div id="import-dropzone-wrapper" class="page-subsection-wrapper">
<div id="upload-wrapper">
<div id="upload-icon-stack">
<img id="import-icon-base" src="/static/res/import.svg" alt="Import Icon">
<img id="import-icon-hover" src="/static/res/import-hover.svg" alt="Import Icon Hover">
</div>
</div>
<div id="import-console">
</div>
</div>
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/import.js"></script>
</body>
</html>

View file

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project</title>
<link rel="stylesheet" href="styles/index.css" />
</head>
<body>
<div id="navbar">
<template data-template="navbar: List<Basic>">
<div id="navbar-section-${section}" class="navbar-section">
<template data-template="navbar-section-${section}: List<Basic>">
<div id="navbar-item-${item}" class="navbar-item" data-page-name="${item}">
${title}
</div>
</template>
</div>
</template>
</div>
<div id="content-body">
<template data-template="content-body: List<Basic>">
<div id="${page}-page" class="page">
<div class="page-header">
<p class="page-header-text">${title}</p>
<div class="page-header-separator"></div>
</div>
<template data-template="extern-${page}-page: Extern"></template>
</div>
</template>
</div>
<script src="scripts/template.js"></script>
<script src="scripts/index.js"></script>
</body>
</html>

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project | Login</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/login.css" />
</head>
<body>
<div id="navbar"></div>
<div id="content-body">
<div id="login-page" class="page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Achievements Project</p>
<div class="page-header-separator"></div>
</div>
</div>
<div id="login-flex">
<div id="login-subsection" class="page-subsection">
<div id="login-elements" class="page-subsection-wrapper">
<div id="login-header" class="page-subheader">
<p id="login-header-text" class="page-subheader-text">Login</p>
<div class="page-subheader-separator"></div>
</div>
<div id="login-form">
<p id="error-message" class="form-row top multiline">Egg</p>
<input id="email" class="login-field form-row" type="text" placeholder="Email"></input>
<input id="username" class="login-field form-row" type="text" placeholder="Username"></input>
<input id="password" class="login-field form-row" type="password" placeholder="Password"></input>
<input id="confirm" class="login-field form-row" type="password" placeholder="Confirm your password"></input>
<div id="button-row" class="form-row">
<div id="create-user-button" class="ap-button login">Create Account</div>
<div id="guest-login-button" class="ap-button login">Continue as Guest</div>
</div>
<div id="login-button" class="ap-button login form-row">Login</div>
<p id="warning" class="form-row">WARNING!</p>
<p id="warning-message" class="form-row multiline">The security of this project is questionable at best. Please refrain from using any truly sensitive data.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/login.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

View file

@ -1,95 +0,0 @@
const expandTemplates = async () => {
template.apply("navbar").values([
{ section: "left" },
{ section: "right" }
]);
template.apply("navbar-section-left").values([
{ item: "games", title: "Games" },
{ item: "achievements", title: "Achievements" }
]);
template.apply("navbar-section-right").values([
{ item: "profile", title: "Profile" }
]);
template.apply("content-body").values([
{ page: "games", title: "Games" },
{ page: "achievements", title: "Achievements" },
{ page: "profile", title: "Profile" }
]);
template.apply("extern-games-page" ).values("games_page" );
template.apply("extern-achievements-page").values("achievements_page");
template.apply("extern-profile-page" ).values("profile_page" );
template.apply("achievements-page-list" ).fetch("achievements", "https://localhost:4730/achievements");
await template.expand();
};
let pages = null;
const loadPages = () => {
pages = document.querySelectorAll(".page");
}
const connectNavbar = () => {
const navItems = document.querySelectorAll(".navbar-item");
for (const item of navItems) {
item.addEventListener("click", (clickEvent) => {
const navItemPageId = item.dataset.pageName + "-page"
for (const page of pages) {
if (page.id === navItemPageId) {
page.style.display = "block";
} else {
page.style.display = "none";
}
}
});
}
};
const connectProfile = () => {
const games = document.querySelector("#profile-games");
const achievements = document.querySelector("#profile-achievements");
games.children[0].addEventListener("click", (clickEvent) => {
for (const page of pages) {
if (page.id === "games-page") {
page.style.display = "block";
} else {
page.style.display = "none";
}
}
});
achievements.children[0].addEventListener("click", (clickEvent) => {
for (page of pages) {
if (page.id === "achievements-page") {
page.style.display = "block";
} else {
page.style.display = "none";
}
}
});
}
const loadFilters = () => {
const filters = document.querySelectorAll(".list-page-filter");
for (let filter of filters) {
filter.addEventListener("click", (clickEvent) => {
if (filter.classList.contains("selected")) {
filter.classList.remove("selected");
} else {
filter.classList.add("selected");
}
});
}
}
window.addEventListener("load", async (loadEvent) => {
await expandTemplates();
loadPages();
connectNavbar();
connectProfile();
loadFilters();
});

View file

@ -1,198 +0,0 @@
var template = template || {};
template.type = {};
template.type._entryMap = new Map();
template.type.register = (type, callback) => {
if (typeof type !== 'string') {
console.error(`'type' must be a string, recieved: `, type);
} else {
const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/;
const result = type.match(TYPE_REGEX);
if (result === null) {
console.error(`'${type}' is not a valid type id`);
} else {
if (result[2] === undefined) {
result[2] = 0;
} else {
result[2] = result[2].split(/\s*,\s*/).length;
}
const completeType = result[1] + ':' + result[2];
if (template.type._entryMap.get(completeType) === undefined) {
template.type._entryMap.set(completeType, async function() {
await callback.apply(null, Array.from(arguments));
});
} else {
console.error(`${type} is already a registered template!`);
}
}
}
};
// Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/* Intrinsic Templates */
// Basic - Simple search and replace
template.type.register('Basic', (element, map) => {
let html = element.innerHTML;
function applyObject(object, path) {
for (const key in object) {
const regexKey = escapeRegExp(path + key);
html = html.replace(new RegExp(`(?:(?<!\\\\)\\\${${regexKey}})`, 'gm'), object[key]);
if (typeof object[key] === 'object') {
applyObject(object[key], path + key + '.');
}
}
}
applyObject(map, '');
html = html.replace('\\&', '&');
element.outerHTML = html.trim();
});
// Extern - Retrieve template from webserver
template.type.register('Extern', (element, name) => {
return fetch(`templates/${name}.html.template`, {
method: 'GET',
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain'
}
}).then(response => response.text().then((data) => {
element.outerHTML = data;
})).catch(error => {
console.error(`failed to retrieve template '${name}': `, error);
});
});
// List - Iterate over list and emit copy of child for each iteration
template.type.register('List<?>', async (element, subtype, arrayMap) => {
let cumulative = '';
const temp = document.createElement('template');
for (const obj of arrayMap) {
temp.innerHTML = `<template></template>`;
const child = temp.content.children[0];
child.innerHTML = element.innerHTML;
const callback = template.type._entryMap.get(subtype.type);
if (callback === undefined) {
cumulative = '';
console.error(`'${subtype.type}' is not a registered template type`);
} else {
await callback.apply(null, [ child, obj ]);
}
cumulative = cumulative + temp.innerHTML.trim();
}
element.outerHTML = cumulative;
});
template._entryMap = new Map();
template.apply = function(pattern, promise) {
if (typeof pattern !== 'string') {
console.error('pattern must be a string, received: ', pattern);
} else {
return new template.apply.applicators(pattern);
}
};
template.apply.applicators = class {
constructor(pattern) {
this._pattern = pattern;
}
_apply(asyncArgs) {
template._entryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs);
}
values(...args) {
this._apply(async () => Array.from(args));
}
promise(promise) {
let args = null;
promise = promise.then(data => args = [ data ]);
this._apply(async () => args || promise);
}
fetch(dataProcessor, url, options) {
if (typeof dataProcessor === 'string') {
const path = dataProcessor;
dataProcessor = data => {
for (const id of path.split(/\./)) {
data = data[id];
if (data === undefined) {
throw `invalid path '${path}'`;
}
}
return data;
};
};
this.promise(
fetch(url, options || { method: 'GET', mode: 'cors' })
.then(response => response.json())
.then(data => dataProcessor(data))
);
}
};
(() => {
const parseType = (type) => {
let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/);
let id = result[1];
result = result[2] ? result[2].split(/\s*,\s*/).map(parseType) : [];
return { type: id + ':' + result.length, params: result };
};
const findTemplates = (element) =>
Array
.from(element.querySelectorAll('template'))
.filter(child => Boolean(child.dataset.template))
.map(child => {
const data = child.dataset.template.split(/\s*:\s*/);
return {
id: data[0],
typeCapture: parseType(data[1] || 'Begin'),
element: child
};
});
const expand = async (element) => {
let children = findTemplates(element);
let promises = [];
let parents = new Set();
for (const child of children) {
for (const [pattern, argsCallback] of template._entryMap) {
await argsCallback().then(args => {
if (pattern.test(child.id)) {
const callback = template.type._entryMap.get(child.typeCapture.type);
if (typeof callback !== 'function') {
console.error(`'${child.typeCapture.type}' is not a registered template type`);
} else {
let params = Array.from(args)
for (const subtype of child.typeCapture.params) {
params.unshift(subtype);
}
params.unshift(child.element);
let parent = child.element.parentElement;
if (!parents.has(parent)) {
parents.add(parent);
}
promises.push(callback.apply(null, params));
}
}
}).catch(error => {
console.error('failed to retrieve arguments: ', error);
});
}
}
await Promise.all(promises);
promises = [];
for (const parent of parents) {
promises.push(expand(parent));
}
await Promise.all(promises);
};
template.expand = async () => expand(document.children[0]);
})();

View file

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/search.css" />
<link rel="stylesheet" href="/static/styles/search_achievements.css" />
</head>
<body>
<div id="navbar">
<template data-template="navbar: List<Basic>">
<div id="navbar-section-${section}" class="navbar-section">
<template data-template="navbar-section-${section}: List<Basic>">
<div id="navbar-item-${item}" class="navbar-item" data-page-name="${item}">
${title}
</div>
</template>
</div>
</template>
</div>
<div id="content-body">
<div id="search-achievements-page" class="search page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Achievement Search</p>
<div class="page-header-separator"></div>
</div>
</div>
<div class="page-subsection">
<div id="list-page-search-filters">
<div id="list-page-search-dropdown">
<div id="search-wrapper" class="page-subsection-wrapper">
<div id="list-page-search-pair" class="list-page-search page-subsection-chunk">
<label id="achievement-search-button" for="achievement-search">Search</label>
<input id="achievement-search-field" type="text" placeholder="Game Name, Achievement Name" name="achievement-search"/>
</div>
</div>
<div id="filter-dropdown-wrapper" class="page-subsection-wrapper">
<div id="filter-dropdown-stack">
<img id="filter-dropdown-button" src="/static/res/dropdown.svg" alt="Dropdown Button"/>
<img id="filter-dropdown-button-hover" src="/static/res/dropdown-hover.svg" alt="Dropdown Button"/>
</div>
</div>
</div>
<div id="list-page-filters-flex">
<div id="personal-filters" class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Personal</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div id="completed-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Completed By Me</p>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Difficulty</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div class="list-page-filter">
<p class="list-page-filter-label">Min. Completion</p>
<input id="min-completion-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max. Completion</p>
<input id="max-completion-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Min. Difficulty</p>
<input id="min-difficulty-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max. Difficulty</p>
<input id="max-difficulty-filter" type="text" class="list-page-filter-param"></input>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Quality</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div class="list-page-filter">
<p class="list-page-filter-label">Min. Quality</p>
<input id="min-quality-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max. Quality</p>
<input id="max-quality-filter" type="text" class="list-page-filter-param"></input>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="list-page-partitions">
<div class="list-page-list-partition page-subsection-wrapper">
<div class="page-subsection-chunk">
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon"></p>
<p id="achievement-header-game" class="list-page-entry-text achievement-game-name">Game</p>
<p id="achievement-header-name" class="list-page-entry-text achievement-name">Name</p>
<p id="achievement-header-completion" class="list-page-entry-text achievement-completion">Completion Rate</p>
<p id="achievement-header-difficulty" class="list-page-entry-text achievement-difficulty">Difficulty</p>
<p id="achievement-header-quality" class="list-page-entry-text achievement-quality">Quality</p>
</div>
<template id="achievement-list-template" data-template="achievements-page-list: List<Basic>">
<div class="list-page-entry achievement" data-id="${achievement_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/achievement/${achievement_id}/image" alt="Achievement Icon"></img>
<p class="list-page-entry-text achievement-game-name">${game_name}</p>
<p class="list-page-entry-text achievement-name">${achievement_name}</p>
<p class="list-page-entry-text achievement-completion">${completion}</p>
<p class="list-page-entry-text achievement-difficulty">${difficulty}</p>
<p class="list-page-entry-text achievement-quality">${quality}</p>
</div>
</template>
</div>
</div>
</div>
</div>
<img id="loading-results" class="ap-loading" src="/static/res/loading.svg" alt="Loading Symbol" />
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/search.js"></script>
<script src="/static/scripts/search_achievements.js"></script>
</body>
</html>

View file

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/search.css" />
<link rel="stylesheet" href="/static/styles/search_games.css" />
</head>
<body>
<div id="navbar">
<template data-template="navbar: List<Basic>">
<div id="navbar-section-${section}" class="navbar-section">
<template data-template="navbar-section-${section}: List<Basic>">
<div id="navbar-item-${item}" class="navbar-item" data-page-name="${item}">
${title}
</div>
</template>
</div>
</template>
</div>
<div id="content-body">
<div id="search-games-page" class="search page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Game Search</p>
<div class="page-header-separator"></div>
</div>
</div>
<div class="page-subsection">
<div id="list-page-search-filters">
<div id="list-page-search-dropdown">
<div id="search-wrapper" class="page-subsection-wrapper">
<div id="list-page-search-pair" class="list-page-search page-subsection-chunk">
<label id="game-search-button" for="game-search">Search</label>
<input id="game-search-field" type="text" placeholder="Name" name="game-search"/>
</div>
</div>
<div id="filter-dropdown-wrapper" class="page-subsection-wrapper">
<div id="filter-dropdown-stack">
<img id="filter-dropdown-button" src="/static/res/dropdown.svg" alt="Dropdown Button"/>
<img id="filter-dropdown-button-hover" src="/static/res/dropdown-hover.svg" alt="Dropdown Button"/>
</div>
</div>
</div>
<div id="list-page-filters-flex">
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Me</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div id="owned-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Owned By Me</p>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Achievements</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div class="list-page-filter">
<p class="list-page-filter-label">Min Avg. Completion</p>
<input id="min-avg-completion-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max Avg. Completion</p>
<input id="max-avg-completion-filter" type="text" class="list-page-filter-param"></input>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Users</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div class="list-page-filter">
<p class="list-page-filter-label">Min Num Owners</p>
<input id="min-num-owners-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max Num Owners</p>
<input id="max-num-owners-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Min Num Perfects</p>
<input id="min-num-perfects-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max Num Perfects</p>
<input id="max-num-perfects-filter" type="text" class="list-page-filter-param"></input>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="list-page-partitions">
<div class="list-page-list-partition page-subsection-wrapper">
<div class="page-subsection-chunk">
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon game"></p>
<p class="list-page-entry-text game-name">Game</p>
<p class="list-page-entry-text game-achievement-count">Achievement Count</p>
<p class="list-page-entry-text game-avg-completion">Avg. Completion</p>
<p class="list-page-entry-text game-num-owners">Num Owners</p>
<p class="list-page-entry-text game-num-perfects">Num Perfects</p>
</div>
<template id="game-list-template" data-template="games-page-list: List<Basic>">
<div class="list-page-entry">
<img class="list-page-entry-icon lazy-img game" data-src="/api/game/${game_id}/image" alt="Game Thumbnail"></img>
<p class="list-page-entry-text game-name">${game_name}</p>
<p class="list-page-entry-text game-achievement-count">${achievement_count}</p>
<p class="list-page-entry-text game-avg-completion">${avg_completion}</p>
<p class="list-page-entry-text game-num-owners">${num_owners}</p>
<p class="list-page-entry-text game-num-perfects">${num_perfects}</p>
</div>
</template>
</div>
</div>
</div>
</div>
<img id="loading-results" class="ap-loading" src="/static/res/loading.svg" alt="Loading Symbol" />
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/search.js"></script>
<script src="/static/scripts/search_games.js"></script>
</body>
</html>

View file

@ -0,0 +1,130 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/search.css" />
<link rel="stylesheet" href="/static/styles/search_users.css" />
</head>
<body>
<div id="navbar">
<template data-template="navbar: List<Basic>">
<div id="navbar-section-${section}" class="navbar-section">
<template data-template="navbar-section-${section}: List<Basic>">
<div id="navbar-item-${item}" class="navbar-item" data-page-name="${item}">
${title}
</div>
</template>
</div>
</template>
</div>
<div id="content-body">
<div id="search-users-page" class="search page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">User Search</p>
<div class="page-header-separator"></div>
</div>
</div>
<div class="page-subsection">
<div id="list-page-search-filters">
<div id="list-page-search-dropdown">
<div id="search-wrapper" class="page-subsection-wrapper">
<div id="list-page-search-pair" class="list-page-search page-subsection-chunk">
<label id="user-search-button" for="user-search">Search</label>
<input id="user-search-field" type="text" placeholder="Name" name="user-search"/>
</div>
</div>
<div id="filter-dropdown-wrapper" class="page-subsection-wrapper">
<div id="filter-dropdown-stack">
<img id="filter-dropdown-button" src="/static/res/dropdown.svg" alt="Dropdown Button"/>
<img id="filter-dropdown-button-hover" src="/static/res/dropdown-hover.svg" alt="Dropdown Button"/>
</div>
</div>
</div>
<div id="list-page-filters-flex">
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Games</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div class="list-page-filter">
<p class="list-page-filter-label">Min Owned</p>
<input id="min-owned-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max Owned</p>
<input id="max-owned-filter" type="text" class="list-page-filter-param"></input>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Achievements</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div class="list-page-filter">
<p class="list-page-filter-label">Min Completed</p>
<input id="min-completed-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max Completed</p>
<input id="max-completed-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Min Avg. Completion</p>
<input id="min-avg-completion-filter" type="text" class="list-page-filter-param"></input>
</div>
<div class="list-page-filter">
<p class="list-page-filter-label">Max Avg. Completion</p>
<input id="max-avg-completion-filter" type="text" class="list-page-filter-param"></input>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="list-page-partitions">
<div class="list-page-list-partition page-subsection-wrapper">
<div class="page-subsection-chunk">
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon"></p>
<p class="list-page-entry-text user-username">Username</p>
<p class="list-page-entry-text user-game-count">Game Count</p>
<p class="list-page-entry-text user-achievement-count">Achievement Count</p>
<p class="list-page-entry-text user-avg-completion">Avg. Completion</p>
<p class="list-page-entry-text user-perfect-games">Perfect Games</p>
</div>
<template id="user-list-template" data-template="user-page-list: List<Basic>">
<div class="list-page-entry user" data-id="${user_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/user/${user_id}/image" alt="User Image"></img>
<p class="list-page-entry-text user-username">${username}</p>
<p class="list-page-entry-text user-game-count">${game_count}</p>
<p class="list-page-entry-text user-achievement-count">${achievement_count}</p>
<p class="list-page-entry-text user-avg-completion">${avg_completion}</p>
<p class="list-page-entry-text user-perfect-games">${perfect_games}</p>
</div>
</template>
</div>
</div>
</div>
</div>
<img id="loading-results" class="ap-loading" src="/static/res/loading.svg" alt="Loading Symbol" />
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/search.js"></script>
<script src="/static/scripts/search_users.js"></script>
</body>
</html>

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M1717.59861,392.24859l-65.17222-65.17219a81.91631,81.91631,0,0,0-115.84716,0L1020.675,842.98065,504.77075,327.07642a81.91631,81.91631,0,0,0-115.84717,0L323.7514,392.2486a81.91631,81.91631,0,0,0,0,115.84716L839.65564,1024,323.75141,1539.90429a81.91631,81.91631,0,0,0,0,115.84717l65.17216,65.17214a81.91632,81.91632,0,0,0,115.84716,0l515.90425-515.9043,515.90425,515.90431a81.91633,81.91633,0,0,0,115.84715,0l65.1722-65.17216a81.91631,81.91631,0,0,0,0-115.84718L1201.69434,1024l515.90428-515.90423A81.91632,81.91632,0,0,0,1717.59861,392.24859Z"/></svg>

After

Width:  |  Height:  |  Size: 716 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M1717.59861,392.24859l-65.17222-65.17219a81.91631,81.91631,0,0,0-115.84716,0L1020.675,842.98065,504.77075,327.07642a81.91631,81.91631,0,0,0-115.84717,0L323.7514,392.2486a81.91631,81.91631,0,0,0,0,115.84716L839.65564,1024,323.75141,1539.90429a81.91631,81.91631,0,0,0,0,115.84717l65.17216,65.17214a81.91632,81.91632,0,0,0,115.84716,0l515.90425-515.9043,515.90425,515.90431a81.91633,81.91633,0,0,0,115.84715,0l65.1722-65.17216a81.91631,81.91631,0,0,0,0-115.84718L1201.69434,1024l515.90428-515.90423A81.91632,81.91632,0,0,0,1717.59861,392.24859Z"/></svg>

After

Width:  |  Height:  |  Size: 716 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><style>.cls-1{fill:#44444c;}</style></defs><path class="cls-1" d="M500,0C223.8576,0,0,223.8576,0,500c0,276.14233,223.8576,500,500,500s500-223.85767,500-500C1000,223.8576,776.1424,0,500,0Zm0,955C248.71045,955,45,751.28955,45,500S248.71045,45,500,45,955,248.71045,955,500,751.28955,955,500,955Z"/></svg>

After

Width:  |  Height:  |  Size: 404 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><polygon class="cls-1" points="1717.02 0 1354.981 0 512 842.981 330.98 1024 1354.98 2048 1717.02 2048 1717.02 1685.961 1055.058 1024 1717.02 362.039 1717.02 0"/></svg>

After

Width:  |  Height:  |  Size: 310 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><polygon class="cls-1" points="1717.02 0 1354.981 0 512 842.981 330.98 1024 1354.98 2048 1717.02 2048 1717.02 1685.961 1055.058 1024 1717.02 362.039 1717.02 0"/></svg>

After

Width:  |  Height:  |  Size: 310 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M1776.471,452.54834l-645.29239,645.29238a56.9113,56.9113,0,0,1-40.24239,16.66895H990.40167a56.91135,56.91135,0,0,1-56.91134-56.91134V957.06378a56.9113,56.9113,0,0,1,16.66895-40.24239L1595.45166,271.529Z"/><path class="cls-1" d="M1534.54675,1056.511v479.48889H512.00012V513.45325H991.48907l256.00007-256.00006H401.27344A145.273,145.273,0,0,0,256.00012,402.72656v1244a145.273,145.273,0,0,0,145.27332,145.27332h1244a145.27292,145.27292,0,0,0,145.27331-145.27332V800.511Z"/><path class="cls-1" d="M1738.98066,53.01934h203.9098a52.0902,52.0902,0,0,1,52.0902,52.0902v151.8196a52.0902,52.0902,0,0,1-52.0902,52.0902h-203.9098a0,0,0,0,1,0,0v-256a0,0,0,0,1,0,0Z" transform="translate(418.82598 1373.17402) rotate(-45)"/></svg>

After

Width:  |  Height:  |  Size: 882 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M1776.471,452.54834l-645.29239,645.29238a56.9113,56.9113,0,0,1-40.24239,16.66895H990.40167a56.91135,56.91135,0,0,1-56.91134-56.91134V957.06378a56.9113,56.9113,0,0,1,16.66895-40.24239L1595.45166,271.529Z"/><path class="cls-1" d="M1534.54675,1056.511v479.48889H512.00012V513.45325H991.48907l256.00007-256.00006H401.27344A145.273,145.273,0,0,0,256.00012,402.72656v1244a145.273,145.273,0,0,0,145.27332,145.27332h1244a145.27292,145.27292,0,0,0,145.27331-145.27332V800.511Z"/><path class="cls-1" d="M1738.98066,53.01934h203.9098a52.0902,52.0902,0,0,1,52.0902,52.0902v151.8196a52.0902,52.0902,0,0,1-52.0902,52.0902h-203.9098a0,0,0,0,1,0,0v-256a0,0,0,0,1,0,0Z" transform="translate(418.82598 1373.17402) rotate(-45)"/></svg>

After

Width:  |  Height:  |  Size: 882 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M500,904C276.877,904,96,723.12305,96,500S276.877,96,500,96V0C223.8576,0,0,223.8576,0,500c0,276.14233,223.8576,500,500,500A498.43514,498.43514,0,0,0,853.55334,853.55334l-67.8822-67.8822A402.7356,402.7356,0,0,1,500,904Z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M170.90729,1032.66212l72.83391-72.83391a76.49864,76.49864,0,0,1,108.18542,0l388.18146,388.18146,955.9653-955.9653a76.49864,76.49864,0,0,1,108.18542,0l72.83391,72.83391a76.49865,76.49865,0,0,1,0,108.18543L740.10808,1710.04834,170.90729,1140.84755A76.49865,76.49865,0,0,1,170.90729,1032.66212Z"/></svg>

After

Width:  |  Height:  |  Size: 466 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M170.90729,1032.66212l72.83391-72.83391a76.49864,76.49864,0,0,1,108.18542,0l388.18146,388.18146,955.9653-955.9653a76.49864,76.49864,0,0,1,108.18542,0l72.83391,72.83391a76.49865,76.49865,0,0,1,0,108.18543L740.10808,1710.04834,170.90729,1140.84755A76.49865,76.49865,0,0,1,170.90729,1032.66212Z"/></svg>

After

Width:  |  Height:  |  Size: 466 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,167 @@
let achievementId = window.location.pathname.split('/').pop();
let isReturn = false;
let achievementData = null;
let myRating = {};
const loadAchievement = () => {
if (myRating.invalid) {
document.querySelector("#achievement-rating").remove();
}
const description = document.querySelector("#achievement-description-text");
if (description.textContent === '') {
description.remove();
}
// Canvasing
const completionCanvas = document.querySelector("#achievement-completion-canvas");
const STROKE_WIDTH = 0.18;
const style = window.getComputedStyle(completionCanvas);
const context = completionCanvas.getContext('2d');
const drawCanvas = () => achievementData.then(data => {
const width = Number(style.getPropertyValue('width').slice(0, -2));
const height = width;
context.canvas.width = width;
context.canvas.height = height;
context.clearRect(0, 0, width, height);
context.strokeStyle = root.getProperty('--accent-value3');
context.lineWidth = (width / 2) * STROKE_WIDTH;
context.beginPath();
context.arc(width / 2, height / 2, (width / 2) * (1 - STROKE_WIDTH / 2), -0.5 * Math.PI, (-0.5 + (data.completion === null ? 0 : (data.completion / 100) * 2)) * Math.PI);
context.stroke();
});
window.addEventListener('resize', drawCanvas);
drawCanvas();
if (!myRating.invalid) {
const saveReview = document.querySelector("#rating-save-stack");
const myDifficulty = document.querySelector("#achievement-difficulty-rating-text");
const myQuality = document.querySelector("#achievement-quality-rating-text");
const myReview = document.querySelector("#achievement-review-rating-text");
const reviewInput = () => {
saveReview.style.display = 'block';
}
myDifficulty.addEventListener('input', reviewInput);
myQuality.addEventListener('input', reviewInput);
myReview.addEventListener('input', reviewInput);
const saveInputOnEnter = (keyEvent) => {
if (keyEvent.key === 'Enter') {
saveReview.click();
}
}
myDifficulty.addEventListener('keydown', saveInputOnEnter);
myQuality.addEventListener('keydown', saveInputOnEnter);
saveReview.addEventListener('click', (clickEvent) => {
let successful = true;
const difficulty = Number(myDifficulty.value);
const quality = Number(myQuality.value );
if ((Number.isNaN(difficulty) && myDifficulty.value !== '') || difficulty < 0 || difficulty > 10) {
myDifficulty.style.backgroundColor = 'var(--error)';
successful = false;
}
if ((Number.isNaN(quality) && myQuality.value !== '') || quality < 0 || quality > 10) {
myQuality.style.backgroundColor = 'var(--error)';
successful = false;
}
if (successful) {
myDifficulty.style.backgroundColor = 'var(--foreground)';
myQuality.style.backgroundColor = 'var(--foreground)';
saveReview.style.display = 'none';
fetch(`/api/achievement/${achievementId}/rating/${session.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionKey: session.key,
difficulty: difficulty,
quality: quality,
review: myReview.value
})
})
.then(response => {
if (response.status === 401) {
responese.json().then(data => {
myDifficulty.value = data.difficulty ? data.difficulty : '';
myQuality.value = data.quality ? data.quality : '';
myReview.value = data.review ? data.review : '';
});
}
});
}
});
}
{
const ratings = document.querySelectorAll(".list-page-entry.rating");
for (const rating of ratings) {
rating.addEventListener("click", (clickEvent) => {
window.location.href = `/user/${rating.dataset.id}`;
});
}
}
}
const expandTemplates = async () => {
await commonTemplates();
if (session.key) {
myRating = await fetch(`/api/achievement/${achievementId}/rating/${session.id}`, { method: 'GET' })
.then(response => {
if (response.status !== 200) {
return { invalid: true };
} else {
return response.json();
}
});
} else {
myRating = { invalid: true };
}
template.apply("achievement-page").promise(achievementData.then(data => ({
id: achievementId,
name: data.name,
description: data.description ? data.description : '',
completion: data.completion === null ? "N/A" : `${data.completion}%`,
difficulty: data.difficulty === null ? "N/A" : `${data.difficulty} / 10`,
quality: data.quality === null ? "N/A" : `${data.quality} / 10`,
my_difficulty: myRating.difficulty ? myRating.difficulty : '',
my_quality: myRating.quality ? myRating.quality : '',
my_review: myRating.review ? myRating.review : '',
})));
template.apply("rating-list").promise(achievementData.then(data => data.ratings.map(data => ({
user_id: data.userId,
user_username: data.username,
user_difficulty: data.difficulty,
user_quality: data.quality,
user_review: data.review
}))));
}
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
var importing = document.querySelector("#importing");
if (/\d+/.test(achievementId)) {
achievementId = Number(achievementId);
} else {
// Handle error
}
importing.remove();
achievementData = fetch(`/api/achievement/${achievementId}`, { method: 'GET' })
.then(response => response.json());
await expandTemplates();
await template.expand();
loadLazyImages();
connectNavbar();
loadAchievement();
});

View file

@ -0,0 +1,120 @@
let root = null;
const loadRoot = () => {
const rootElement = document.documentElement;
root = {};
root.getProperty = (name) => window.getComputedStyle(document.documentElement).getPropertyValue(name);
root.setProperty = (name, value) => {
rootElement.style.setProperty(name, value);
}
};
let session = { id: null };
const clearSession = () => session = { id: null };
const loadSession = async () => {
window.addEventListener('beforeunload', (beforeUnloadEvent) => {
window.sessionStorage.setItem('session', JSON.stringify(session));
});
session = JSON.parse(window.sessionStorage.getItem('session')) || { id: -1 };
if (session.hue) {
root.setProperty('--accent-hue', session.hue);
}
if (session.id !== null) {
await fetch(`/api/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key, id: session.id })
})
.then(async response => ({ status: response.status, data: await response.json() }))
.then(response => {
if (response.status !== 200 && window.location.pathname !== "/login") {
session.id = null;
session.key = null;
if (session.id !== -1) {
window.location.href = "/login";
}
} else {
session.key = response.data.key;
session.id = response.data.id;
if (session.id === -1 && window.location.pathname !== '/import') {
window.location.href = '/import';
}
}
});
}
};
const authenticate = (obj) => {
obj.sessionKey = session.key;
obj.userId = session.id;
return obj;
}
const loadCommon = async () => {
loadRoot();
await loadSession();
}
const commonTemplates = async () => {
template.apply("navbar").values([
{ section: "left" },
{ section: "right" }
]);
template.apply("navbar-section-left").values([
{ item: "achievements", title: "Achievements" },
{ item: "users", title: "Users" },
{ item: "games", title: "Games" },
{ item: "import", title: "Import" }
]);
if (session.id !== -1 && session.id !== null) {
template.apply("navbar-section-right").values([
{ item: "profile", title: "Profile" },
{ item: "logout", title: "Logout" }
]);
} else {
template.apply("navbar-section-right").values([
{ item: "login", title: "Login / Create Account" }
]);
}
};
const loadLazyImages = () => {
const imgs = document.querySelectorAll(".lazy-img");
for (const img of imgs) {
img.src = img.dataset.src;
}
}
const connectNavbar = () => {
if (session.id !== -1) {
const navItems = document.querySelectorAll(".navbar-item");
if (!session.admin) {
document.querySelector("#navbar-item-import").remove();
}
for (const item of navItems) {
if (item.dataset.pageName === "logout") {
item.addEventListener("click", (clickEvent) => {
fetch(`/api/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
});
clearSession();
window.location.href = "/login";
});
} else if (item.dataset.pageName === "profile") {
item.addEventListener("click", (clickEvent) => window.location.href = `/user/${session.id}`);
} else {
item.addEventListener("click", (clickEvent) => window.location.href = `/${item.dataset.pageName}`);
}
}
}
};

View file

@ -0,0 +1,105 @@
let consoleTop = true;
let importConsole = null;
const appendLine = (line) => {
const template = document.createElement("template");
template.innerHTML = `<p class="console-entry ${consoleTop ? 'top' : ''}">${line}</p>`
importConsole.appendChild(template.content.firstElementChild);
consoleTop = false;
};
const loadConsole = () => {
importConsole = document.querySelector("#import-console");
const dropzone = document.querySelector("#import-dropzone");
const uploadWrapper = document.querySelector("#upload-wrapper");
const upload = (dropEvent) => {
dropEvent.preventDefault();
dropzone.classList.remove('active');
if (dropEvent.dataTransfer.files) {
const file = dropEvent.dataTransfer.files[0];
if (file.type === 'application/json') {
importConsole.style.display = 'block';
uploadWrapper.style.display = 'none';
file.text().then(data => JSON.parse(data)).then(data => {
let uploads = Promise.resolve();
for (let i = 0; i < data.platforms.length; ++i) {
const platform = data.platforms[i];
uploads = uploads
.then(() => {
appendLine(`(${i + 1}/${data.platforms.length}) Creating platform: ${platform.name}`);
}).then(() => fetch(
'/api/import/platform', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticate(platform))
}
)
);
}
for (let i = 0; i < data.users.length; ++i) {
const user = data.users[i];
const userPlatforms = user.platforms;
delete user.platforms;
uploads = uploads
.then(() => {
appendLine(`(${i + 1}/${data.users.length}) Creating user: ${user.username}`);
}).then(() => fetch(
'/api/import/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticate(user))
}
)
);
for (let j = 0; j < userPlatforms.length; ++j) {
const platform = userPlatforms[j];
platform.userEmail = user.email;
uploads = uploads
.then(() => {
appendLine(`&nbsp;&nbsp;&nbsp;&nbsp;(${j + 1}/${userPlatforms.length}) Importing platform data: ${data.platforms[platform.platformId].name}`);
}).then(() => fetch(
'/api/import/user/platform', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticate(platform))
}
)
);
}
}
uploads = uploads.then(() => {
if (session.id === -1) {
clearSession();
window.location.href = '/login';
} else {
importConsole.innerHTML = '';
importConsole.style.display = 'none';
uploadWrapper.style.display = 'block';
}
});
});
}
}
};
dropzone.addEventListener("drop", upload);
dropzone.addEventListener("dragover", (dragEvent) => {
dragEvent.preventDefault();
});
};
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
await commonTemplates();
await template.expand();
connectNavbar();
loadConsole();
});

View file

@ -0,0 +1,185 @@
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
if (session.key) {
window.location.href = '/';
}
const fields = {
email: document.querySelector("#email" ),
username: document.querySelector("#username"),
password: document.querySelector("#password"),
confirm: document.querySelector("#confirm" )
};
fields.email.focus();
const createUser = document.querySelector("#create-user-button");
const login = document.querySelector("#login-button");
const guest = document.querySelector("#guest-login-button");
const header = document.querySelector("#login-header-text");
const error = document.querySelector("#error-message");
if (!session.key && session.id) {
error.style.display = "block";
error.textContent = "You have been signed out due to inactivity";
}
const raiseError = (errorFields, message) => {
for (const key in fields) {
if (errorFields.includes(key)) {
fields[key].classList.add("error");
} else {
fields[key].classList.remove("error");
}
}
error.style.display = "block";
error.textContent = message;
}
let frozen = false;
const freeze = () => {
frozen = true;
createUser.classList.add("disabled");
login.classList.add("disabled");
guest.classList.add("disabled");
};
const unfreeze = () => {
frozen = false;
createUser.classList.remove("disabled");
login.classList.remove("disabled");
guest.classList.remove("disabled");
};
const switchToCreateAction = (clickEvent) => {
if (!frozen) {
fields.username.style.display = "block";
fields.confirm.style.display = "block";
header.textContent = "Create Account";
createUser.removeEventListener("click", switchToCreateAction);
createUser.addEventListener("click", createUserAction);
login.removeEventListener("click", loginAction);
login.addEventListener("click", switchToLoginAction);
activeAction = createUserAction;
}
};
const createUserAction = (clickEvent) => {
if (!frozen) {
if (fields.email.value === '') {
raiseError([ "email" ], "Email cannot be empty");
} else if (fields.username.value === '') {
raiseError([ "username" ], "Username cannot be empty");
} else if (fields.password.value !== fields.confirm.value) {
raiseError([ "password", "confirm" ], "Password fields did not match");
} else if (fields.password.value === '') {
raiseError([ "password", "confirm" ], "Password cannot be empty");
} else {
freeze();
fetch(`/api/auth/create_user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: fields.email.value, username: fields.username.value, password: fields.password.value })
})
.then(async response => ({ status: response.status, data: await response.json() }))
.then(response => {
const data = response.data;
if (response.status === 201) {
session = data;
window.location.href = `/user/${session.id}`;
} else if (response.status === 500) {
raiseError([], "Internal server error :(");
} else {
if (data.code === 1) {
raiseError([ "email" ], "A user with that email is already registered");
fields.email.value = '';
} else if (data.code === 2) {
raiseError([ "email" ], "Invalid email address");
fields.email.value = '';
} else {
raiseError([ "email" ], "Server is bad :p");
fields.email.value = '';
}
}
})
.catch(error => {
console.error(error);
raiseError([], "Server error :(");
}).then(unfreeze);
}
}
};
createUser.addEventListener("click", switchToCreateAction);
const switchToLoginAction = (clickEvent) => {
if (!frozen) {
fields.username.style.display = "none";
fields.confirm.style.display = "none";
header.textContent = "Login";
createUser.removeEventListener("click", createUserAction);
createUser.addEventListener("click", switchToCreateAction);
login.removeEventListener("click", switchToLoginAction);
login.addEventListener("click", loginAction);
activeAction = loginAction;
}
};
const loginAction = (clickEvent) => {
if (!frozen) {
if (fields.email.value === '') {
raiseError([ "email" ], "Email cannot be empty");
} else if (fields.password.value === '') {
raiseError([ "password" ], "Password cannot be empty");
} else {
freeze();
fetch(`/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: fields.email.value, password: fields.password.value })
})
.then(async response => ({ status: response.status, data: await response.json() }))
.then(response => {
const data = response.data;
if (response.status === 200) {
session = data;
window.location.href = "/";
} else if (response.status === 500) {
raiseError([], "Internal server error :(");
} else {
raiseError([ "email", "password" ], "Email or password is incorrect");
fields.password.value = '';
}
})
.catch(error => {
console.error(error);
raiseError([], "Unknown error :(");
}).then(unfreeze);
}
}
};
login.addEventListener("click", loginAction);
guest.addEventListener("click", (clickEvent) => {
if (!frozen) {
window.location.href = '/';
}
});
let activeAction = loginAction;
for (const field in fields) {
fields[field].addEventListener("keydown", (keyEvent) => {
if (keyEvent.key === "Enter") {
activeAction();
}
})
}
});

View file

@ -0,0 +1,26 @@
const expandTemplates = async () => {
await commonTemplates();
}
const loadFilters = () => {
const filtersButton = document.querySelector("#filter-dropdown-stack");
const filtersSection = document.querySelector("#list-page-filters-flex");
filtersButton.addEventListener("click", (clickEvent) => {
filtersButton.classList.toggle("active");
filtersSection.classList.toggle("active");
});
const filterCheckboxes = document.querySelectorAll(".list-page-filter-checkbox");
for (const checkbox of filterCheckboxes) {
checkbox.parentElement.addEventListener("click", (clickEvent) => {
checkbox.parentElement.classList.toggle("selected");
})
}
}
const loadCommonSearch = async () => {
await loadCommon();
await expandTemplates();
};

View file

@ -0,0 +1,141 @@
let templateList = null;
let templateText = null;
const saveTemplate = () => {
const templateElement = document.querySelector("#achievement-list-template");
templateList = templateElement.parentElement;
templateText = templateElement.outerHTML;
templateElement.remove();
};
const loadAchievementSearch = () => {
const loading = document.querySelector("#loading-results");
const personal = document.querySelector("#personal-filters");
if (!session) {
personal.style.display = 'none';
}
const searchButton = document.querySelector("#achievement-search-button");
const searchField = document.querySelector("#achievement-search-field" );
const completed = document.querySelector("#completed-filter");
const minCompletion = document.querySelector("#min-completion-filter");
const maxCompletion = document.querySelector("#max-completion-filter");
const minDifficulty = document.querySelector("#min-difficulty-filter");
const maxDifficulty = document.querySelector("#max-difficulty-filter");
const minQuality = document.querySelector("#min-quality-filter" );
const maxQuality = document.querySelector("#max-quality-filter" );
let ordering = 'name';
let direction = true;
let canSearch = true;
const loadList = async () => {
if (canSearch) {
canSearch = false;
const body = {
searchTerm: searchField.value,
userId: completed.classList.contains('selected') ? session.id : null,
completed: completed.classList.contains('selected') ? true : null,
minCompletion: minCompletion.value === '' ? null : Number(minCompletion.value),
maxCompletion: maxCompletion.value === '' ? null : Number(maxCompletion.value),
minDifficulty: minDifficulty.value === '' ? null : Number(minDifficulty.value),
maxDifficulty: maxDifficulty.value === '' ? null : Number(maxDifficulty.value),
minQuality: minQuality.value === '' ? null : Number(minQuality.value ),
maxQuality: maxQuality.value === '' ? null : Number(maxQuality.value ),
ordering: ordering,
orderDirection: direction ? 'ASC' : 'DESC',
};
let successful = true;
if (Number.isNaN(body.minCompletion)) { successful = false; minCompletion.style.backgroundColor = 'var(--error)'; } else { minCompletion.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxCompletion)) { successful = false; maxCompletion.style.backgroundColor = 'var(--error)'; } else { maxCompletion.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.minDifficulty)) { successful = false; minDifficulty.style.backgroundColor = 'var(--error)'; } else { minDifficulty.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxDifficulty)) { successful = false; maxDifficulty.style.backgroundColor = 'var(--error)'; } else { maxDifficulty.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.minQuality )) { successful = false; minQuality.style.backgroundColor = 'var(--error)'; } else { minQuality.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxQuality )) { successful = false; maxQuality.style.backgroundColor = 'var(--error)'; } else { maxQuality.style.backgroundColor = 'var(--foreground)'; }
if (!successful) {
canSearch = true;
return;
}
for (const entry of templateList.querySelectorAll(".list-page-entry")) {
entry.remove();
}
templateList.innerHTML += templateText;
loading.style.display = 'block';
const data = fetch("/api/achievements", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(response => response.json())
template.clear();
template.apply('achievements-page-list').promise(data.then(data => data.map(item => ({
achievement_id: item.ID,
achievement_name: item.name,
game_name: item.game,
completion: item.completion == null ? 'N/A' : `${item.completion}%`,
difficulty: item.difficulty == null ? 'N/A' : `${item.difficulty} / 10`,
quality: item.quality == null ? 'N/A' : `${item.quality} / 10`
}))));
await template.expand();
data.then(data => {
loading.style.display = 'none';
canSearch = true;
loadLazyImages();
const entries = document.querySelectorAll(".list-page-entry.achievement");
for (const entry of entries) {
entry.addEventListener("click", (clickEvent) => {
window.location.href = `/achievement/${entry.dataset.id}`;
});
}
});
const headers = {
game: document.querySelector(".list-page-header > .achievement-game-name" ),
name: document.querySelector(".list-page-header > .achievement-name" ),
completion: document.querySelector(".list-page-header > .achievement-completion"),
difficulty: document.querySelector(".list-page-header > .achievement-difficulty"),
quality: document.querySelector(".list-page-header > .achievement-quality" ),
}
for (const header in headers) {
headers[header].addEventListener("click", (clickEvent) => {
if (ordering === header) {
direction = !direction;
} else {
ordering = header;
direction = true;
}
loadList();
});
}
}
};
searchButton.addEventListener("click", loadList);
searchField.addEventListener("keydown", (keyEvent) => {
if (keyEvent.key === 'Enter') {
loadList();
}
});
loadList();
};
window.addEventListener("load", async (loadEvent) => {
await loadCommonSearch();
saveTemplate();
await template.expand();
connectNavbar();
loadFilters();
await loadAchievementSearch();
});

View file

@ -0,0 +1,134 @@
let templateList = null;
let templateText = null;
const saveTemplate = () => {
const templateElement = document.querySelector("#game-list-template");
templateList = templateElement.parentElement;
templateText = templateElement.outerHTML;
templateElement.remove();
};
const loadGameSearch = () => {
const loading = document.querySelector("#loading-results");
const personal = document.querySelector("#personal-filters");
if (!session) {
personal.style.display = 'none';
}
const searchButton = document.querySelector("#game-search-button");
const searchField = document.querySelector("#game-search-field" );
const owned = document.querySelector("#owned-filter" );
const minAvgCompletion = document.querySelector("#min-avg-completion-filter");
const maxAvgCompletion = document.querySelector("#max-avg-completion-filter");
const minNumOwners = document.querySelector("#min-num-owners-filter" );
const maxNumOwners = document.querySelector("#max-num-owners-filter" );
const minNumPerfects = document.querySelector("#min-num-perfects-filter" );
const maxNumPerfects = document.querySelector("#max-num-perfects-filter" );
let ordering = 'name';
let direction = true;
let canSearch = true;
const loadList = async () => {
if (canSearch) {
canSearch = false;
const body = {
searchTerm: searchField.value,
userId: owned.classList.contains('selected') ? session.id : null,
owned: owned.classList.contains('selected') ? true : null,
minAvgCompletion: minAvgCompletion.value === '' ? null : Number(minAvgCompletion.value),
maxAvgCompletion: maxAvgCompletion.value === '' ? null : Number(maxAvgCompletion.value),
minNumOwners: minNumOwners.value === '' ? null : Number(minNumOwners.value ),
maxNumOwners: maxNumOwners.value === '' ? null : Number(maxNumOwners.value ),
minNumPerfects: minNumPerfects.value === '' ? null : Number(minNumPerfects.value ),
maxNumPerfects: maxNumPerfects.value === '' ? null : Number(maxNumPerfects.value ),
ordering: ordering,
orderDirection: direction ? 'ASC' : 'DESC',
};
let successful = true;
if (Number.isNaN(body.minAvgCompletion)) { successful = false; minAvgCompletion.style.backgroundColor = 'var(--error)'; } else { minAvgCompletion.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxAvgCompletion)) { successful = false; maxAvgCompletion.style.backgroundColor = 'var(--error)'; } else { maxAvgCompletion.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.minNumOwners)) { successful = false; minNumOwners.style.backgroundColor = 'var(--error)'; } else { minNumOwners.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxNumOwners)) { successful = false; maxNumOwners.style.backgroundColor = 'var(--error)'; } else { maxNumOwners.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.minNumPerfects)) { successful = false; minNumPerfects.style.backgroundColor = 'var(--error)'; } else { minNumPerfects.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxNumPerfects)) { successful = false; maxNumPerfects.style.backgroundColor = 'var(--error)'; } else { maxNumPerfects.style.backgroundColor = 'var(--foreground)'; }
if (!successful) {
canSearch = true;
return;
}
for (const entry of templateList.querySelectorAll(".list-page-entry")) {
entry.remove();
}
templateList.innerHTML += templateText;
loading.style.display = 'block';
const data = fetch("/api/games", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(response => response.json())
template.clear();
template.apply('games-page-list').promise(data.then(data => data.map(item => ({
game_id: item.ID,
game_name: item.name,
achievement_count: item.achievement_count,
avg_completion: item.avg_completion == null ? 'N/A' : `${item.avg_completion}%`,
num_owners: item.num_owners,
num_perfects: item.num_perfects,
}))));
await template.expand();
data.then(data => {
loading.style.display = 'none';
canSearch = true;
loadLazyImages();
});
const headers = {
game: document.querySelector(".list-page-header > .game-name" ),
achievement_count: document.querySelector(".list-page-header > .game-achievement-count"),
avg_completion: document.querySelector(".list-page-header > .game-avg-completion" ),
num_owners: document.querySelector(".list-page-header > .game-num-owners" ),
num_perfects: document.querySelector(".list-page-header > .game-num-perfects" ),
}
for (const header in headers) {
headers[header].addEventListener("click", (clickEvent) => {
if (ordering === header) {
direction = !direction;
} else {
ordering = header;
direction = true;
}
loadList();
});
}
}
};
searchButton.addEventListener("click", loadList);
searchField.addEventListener("keydown", (keyEvent) => {
if (keyEvent.key === 'Enter') {
loadList();
}
});
loadList();
};
window.addEventListener("load", async (loadEvent) => {
await loadCommonSearch();
saveTemplate();
await template.expand();
connectNavbar();
loadFilters();
await loadGameSearch();
});

View file

@ -0,0 +1,132 @@
let templateList = null;
let templateText = null;
const saveTemplate = () => {
const templateElement = document.querySelector("#user-list-template");
templateList = templateElement.parentElement;
templateText = templateElement.outerHTML;
templateElement.remove();
};
const loadUserSearch = () => {
const loading = document.querySelector("#loading-results");
const searchButton = document.querySelector("#user-search-button");
const searchField = document.querySelector("#user-search-field" );
const minOwned = document.querySelector("#min-owned-filter" );
const maxOwned = document.querySelector("#max-owned-filter" );
const minCompleted = document.querySelector("#min-completed-filter" );
const maxCompleted = document.querySelector("#max-completed-filter" );
const minAvgCompletion = document.querySelector("#min-avg-completion-filter");
const maxAvgCompletion = document.querySelector("#max-avg-completion-filter");
let ordering = 'name';
let direction = true;
let canSearch = true;
const loadList = async () => {
if (canSearch) {
canSearch = false;
const body = {
searchTerm: searchField.value,
minOwned: minOwned.value === '' ? null : Number(minOwned.value ),
maxOwned: maxOwned.value === '' ? null : Number(maxOwned.value ),
minCompleted: minCompleted.value === '' ? null : Number(minCompleted.value ),
maxCompleted: maxCompleted.value === '' ? null : Number(maxCompleted.value ),
minAvgCompletion: minAvgCompletion.value === '' ? null : Number(minAvgCompletion.value),
maxAvgCompletion: maxAvgCompletion.value === '' ? null : Number(maxAvgCompletion.value),
};
let successful = true;
if (Number.isNaN(body.minOwned )) { successful = false; minOwned.style.backgroundColor = 'var(--error)'; } else { minOwned.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxOwned )) { successful = false; maxOwned.style.backgroundColor = 'var(--error)'; } else { maxOwned.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.minCompleted )) { successful = false; minCompleted.style.backgroundColor = 'var(--error)'; } else { minCompleted.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxCompleted )) { successful = false; maxCompleted.style.backgroundColor = 'var(--error)'; } else { maxCompleted.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.minAvgCompletion)) { successful = false; minAvgCompletion.style.backgroundColor = 'var(--error)'; } else { minAvgCompletion.style.backgroundColor = 'var(--foreground)'; }
if (Number.isNaN(body.maxAvgCompletion)) { successful = false; maxAvgCompletion.style.backgroundColor = 'var(--error)'; } else { maxAvgCompletion.style.backgroundColor = 'var(--foreground)'; }
if (!successful) {
canSearch = true;
return;
}
for (const entry of templateList.querySelectorAll(".list-page-entry")) {
entry.remove();
}
templateList.innerHTML += templateText;
loading.style.display = 'block';
const data = fetch("/api/users", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(response => response.json())
template.clear();
template.apply('user-page-list').promise(data.then(data => data.map(item => ({
user_id: item.id,
username: item.username,
game_count: item.game_count,
achievement_count: item.achievement_count,
avg_completion: item.avg_completion == null ? 'N/A' : `${item.avg_completion}%`,
perfect_games: item.perfect_games
}))));
await template.expand();
data.then(data => {
loading.style.display = 'none';
canSearch = true;
loadLazyImages();
const entries = document.querySelectorAll(".list-page-entry.user");
for (const entry of entries) {
entry.addEventListener("click", (clickEvent) => {
window.location.href = `/user/${entry.dataset.id}`;
});
}
});
const headers = {
username: document.querySelector(".list-page-header > .user-username" ),
game_count: document.querySelector(".list-page-header > .user-game-count" ),
achievement_count: document.querySelector(".list-page-header > .user-achievement-count"),
avg_completion: document.querySelector(".list-page-header > .user-avg-completion" ),
perfect_games: document.querySelector(".list-page-header > .user-perfect-games" ),
}
for (const header in headers) {
headers[header].addEventListener("click", (clickEvent) => {
if (ordering === header) {
direction = !direction;
} else {
ordering = header;
direction = true;
}
loadList();
});
}
}
};
searchButton.addEventListener("click", loadList);
searchField.addEventListener("keydown", (keyEvent) => {
if (keyEvent.key === 'Enter') {
loadList();
}
});
loadList();
};
window.addEventListener("load", async (loadEvent) => {
await loadCommonSearch();
saveTemplate();
await template.expand();
connectNavbar();
loadFilters();
await loadUserSearch();
});

View file

@ -0,0 +1,201 @@
var template = template || {};
(() => {
templateTypeEntryMap = new Map();
template.register = (type, callback) => {
if (typeof type !== 'string') {
console.error(`'type' must be a string, recieved: `, type);
} else {
const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/;
const result = type.match(TYPE_REGEX);
if (result === null) {
console.error(`'${type}' is not a valid type id`);
} else {
if (result[2] === undefined) {
result[2] = 0;
} else {
result[2] = result[2].split(/\s*,\s*/).length;
}
const completeType = result[1] + ':' + result[2];
if (templateTypeEntryMap.get(completeType) === undefined) {
templateTypeEntryMap.set(completeType, async function() {
await callback.apply(null, Array.from(arguments));
});
} else {
console.error(`${type} is already a registered template!`);
}
}
}
};
// Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/* Intrinsic Templates */
// Basic - Simple search and replace
template.register('Basic', (element, map) => {
let html = element.innerHTML;
function applyObject(object, path) {
for (const key in object) {
const regexKey = escapeRegExp(path + key);
html = html.replace(new RegExp(`(?:(?<!\\\\)\\\${${regexKey}})`, 'gm'), object[key]);
if (typeof object[key] === 'object') {
applyObject(object[key], path + key + '.');
}
}
}
applyObject(map, '');
html = html.replace('\\&', '&');
element.outerHTML = html.trim();
});
// Extern - Retrieve template from webserver
template.register('Extern', (element, name) => {
return fetch(`templates/${name}.html.template`, {
method: 'GET',
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain'
}
}).then(response => response.text().then((data) => {
element.outerHTML = data;
})).catch(error => {
console.error(`failed to retrieve template '${name}': `, error);
});
});
// List - Iterate over list and emit copy of child for each iteration
template.register('List<?>', async (element, subtype, arrayMap) => {
let cumulative = '';
const temp = document.createElement('template');
for (const obj of arrayMap) {
temp.innerHTML = `<template></template>`;
const child = temp.content.children[0];
child.innerHTML = element.innerHTML;
const callback = templateTypeEntryMap.get(subtype.type);
if (callback === undefined) {
cumulative = '';
console.error(`'${subtype.type}' is not a registered template`);
} else {
await callback.apply(null, [ child, obj ]);
}
cumulative = cumulative + temp.innerHTML.trim();
}
element.outerHTML = cumulative;
});
templateEntryMap = new Map();
template.apply = function(pattern, promise) {
if (typeof pattern !== 'string') {
console.error('pattern must be a string, received: ', pattern);
} else {
return new template.apply.applicators(pattern);
}
};
template.apply.applicators = class {
constructor(pattern) {
this._pattern = pattern;
}
_apply(asyncArgs) {
templateEntryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs);
}
values(...args) {
this._apply(async () => Array.from(args));
}
promise(promise) {
let args = null;
promise = promise.then(data => args = [ data ]);
this._apply(async () => args || promise);
}
fetch(dataProcessor, url, options) {
if (typeof dataProcessor === 'string') {
const path = dataProcessor;
dataProcessor = data => {
for (const id of path.split(/\./)) {
data = data[id];
if (data === undefined) {
throw `invalid path '${path}'`;
}
}
return data;
};
};
this.promise(
fetch(url, options || { method: 'GET', mode: 'cors' })
.then(response => response.json())
.then(data => dataProcessor(data))
);
}
};
template.clear = () => {
templateEntryMap.clear();
}
const parseType = (type) => {
let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/);
let id = result[1];
result = result[2] ? result[2].split(/\s*,\s*/).map(parseType) : [];
return { type: id + ':' + result.length, params: result };
};
const findTemplates = (element) =>
Array
.from(element.querySelectorAll('template'))
.filter(child => Boolean(child.dataset.template))
.map(child => {
const data = child.dataset.template.split(/\s*:\s*/);
return {
id: data[0],
typeCapture: parseType(data[1] || 'Basic'),
element: child
};
});
const expand = async (element) => {
let children = findTemplates(element);
let promises = [];
let parents = new Set();
for (const child of children) {
for (const [pattern, argsCallback] of templateEntryMap) {
await argsCallback().then(args => {
if (pattern.test(child.id)) {
const callback = templateTypeEntryMap.get(child.typeCapture.type);
if (typeof callback !== 'function') {
console.error(`'${child.typeCapture.type}' is not a registered template`);
} else {
let params = Array.from(args)
for (const subtype of child.typeCapture.params) {
params.unshift(subtype);
}
params.unshift(child.element);
let parent = child.element.parentElement;
if (!parents.has(parent)) {
parents.add(parent);
}
promises.push(callback.apply(null, params));
}
}
}).catch(error => {
console.error('failed to retrieve arguments: ', error);
});
}
}
await Promise.all(promises);
promises = [];
for (const parent of parents) {
promises.push(expand(parent));
}
await Promise.all(promises);
};
template.expand = async () => expand(document.children[0]);
})();

Some files were not shown because too many files have changed in this diff Show more