Added SteamAPI and achievement searching

This commit is contained in:
Gnarwhal 2021-02-18 02:15:09 -05:00
parent b229ff9a15
commit 627cc810ed
Signed by: Gnarwhal
GPG key ID: 0989A73D8C421174
61 changed files with 2781 additions and 903 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
tmp/*

5
backend/.gitignore vendored
View file

@ -23,8 +23,5 @@ src/main/resources/application-local.properties
# Server Keystore
src/main/resources/achievements-ssl-key.p12
# Api Keys
apikeys/
# Program Data
images/
storage/

View file

@ -4,11 +4,15 @@ 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;
import java.time.Duration;
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@EnableScheduling
public class Application {
@ -34,4 +38,9 @@ public class Application {
}
};
}
@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,102 @@
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>();
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());
newGame.setThumbnail("https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/" + game.getAppid() + "/" + game.getImg_logo_url() + ".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();
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);
}
}
}
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,27 @@
package achievements.controllers;
import achievements.services.ImageService;
import achievements.services.AchievementService;
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("/achievement")
public class AchievementController {
@Autowired
private AchievementService achievementService;
@Autowired
private ImageService imageService;
@GetMapping(value = "/{achievement}/image")
public void getProfilePicture(@PathVariable("achievement") int achievement, HttpServletResponse response) {
var icon = achievementService.getIcon(achievement);
imageService.send(icon, "achievement", response);
}
}

View file

@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class LoginController {
public class AuthController {
@Autowired
private AuthenticationService authService;

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

@ -1,42 +1,27 @@
package achievements.controllers;
import achievements.services.ImageService;
import achievements.services.PlatformService;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@RestController
@RequestMapping("/platform")
public class PlatformController {
@Autowired
private PlatformService platforms;
private ImageService imageService;
@Autowired
private PlatformService platformService;
@GetMapping(value = "/platform/image/{id}", produces = "application/json")
public void getPlatformImage(@PathVariable("id") int id, HttpServletResponse response) {
try {
var file = new File("images/platform/" + id + ".png");
if (file.exists()) {
var stream = new FileInputStream(file);
IOUtils.copy(stream, response.getOutputStream());
response.setContentType("image/png");
response.setStatus(200);
response.flushBuffer();
stream.close();
} else {
response.setStatus(HttpStatus.BAD_REQUEST.value());
}
} catch (IOException e) {
e.printStackTrace();
}
@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,26 @@
package achievements.controllers;
import achievements.data.query.SearchAchievements;
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("[]");
}
}
}

View file

@ -2,20 +2,18 @@ package achievements.controllers;
import achievements.data.APError;
import achievements.data.APPostRequest;
import achievements.data.query.AddPlatformRequest;
import achievements.data.query.RemovePlatformRequest;
import achievements.data.query.AddPlatform;
import achievements.data.query.RemovePlatform;
import achievements.data.query.SetUsername;
import achievements.services.ImageService;
import achievements.services.UserService;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@RestController
@RequestMapping("/user")
@ -24,6 +22,9 @@ 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);
@ -45,38 +46,21 @@ public class UserController {
@GetMapping(value = "/{user}/image")
public void getProfilePicture(@PathVariable("user") int user, HttpServletResponse response) {
var pfp = userService.getProfileImageType(user);
if (pfp == null) {
} else {
var file = new File("images/user/" + pfp[0] + "." + pfp[1]);
response.setContentType("image/" + pfp[2]);
try {
var stream = new FileInputStream(file);
IOUtils.copy(stream, response.getOutputStream());
response.flushBuffer();
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
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.setProfileImageType(user, session.getKey(), file.getContentType());
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 (!"unknown".equals(type)) {
var pfp = new FileOutputStream("images/user/" + user + "." + type);
FileCopyUtils.copy(file.getInputStream(), pfp);
pfp.close();
} else if ("success".equals(type)) {
return ResponseEntity.status(HttpStatus.CREATED).body("{ \"code\": 0, \"message\": \"Success\" }");
}
@ -87,7 +71,7 @@ public class UserController {
}
@PostMapping(value = "/{user}/platforms/add", consumes = "application/json", produces = "application/json")
public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatformRequest request) {
public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatform request) {
var result = userService.addPlatform(userId, request);
if (result == 0) {
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
@ -97,7 +81,7 @@ public class UserController {
}
@PostMapping(value = "/{user}/platforms/remove", consumes = "application/json", produces = "application/json")
public ResponseEntity removePlatformForUser(@PathVariable("user") int userId, @RequestBody RemovePlatformRequest request) {
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("{}");

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

@ -1,122 +1,38 @@
package achievements.data;
import achievements.data.query.NumericFilter;
import achievements.data.query.StringFilter;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Achievement {
public static class Query {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("name")
private StringFilter name;
@JsonProperty("stages")
private NumericFilter stages;
@JsonProperty("completion")
private NumericFilter completion;
@JsonProperty("difficulty")
private NumericFilter difficulty;
@JsonProperty("quality")
private NumericFilter quality;
public Query(String sessionKey, StringFilter name, NumericFilter stages, NumericFilter completion, NumericFilter difficulty, NumericFilter quality) {
this.sessionKey = sessionKey;
this.name = name;
this.stages = stages;
this.completion = completion;
this.difficulty = difficulty;
this.quality = quality;
}
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public StringFilter getName() {
return name;
}
public void setName(StringFilter name) {
this.name = name;
}
public NumericFilter getStages() {
return stages;
}
public void setStages(NumericFilter stages) {
this.stages = stages;
}
public NumericFilter getCompletion() {
return completion;
}
public void setCompletion(NumericFilter completion) {
this.completion = completion;
}
public NumericFilter getDifficulty() {
return difficulty;
}
public void setDifficulty(NumericFilter difficulty) {
this.difficulty = difficulty;
}
public NumericFilter getQuality() {
return quality;
}
public void setQuality(NumericFilter quality) {
this.quality = quality;
}
}
@JsonProperty("ID")
private int id;
private int ID;
@JsonProperty("game")
private int gameId;
private String game;
@JsonProperty("name")
private String name;
@JsonProperty("description")
private String description;
@JsonProperty("stages")
private int stages;
@JsonProperty("completion")
private float completion;
private Integer completion;
@JsonProperty("difficulty")
private float difficulty;
private Float difficulty;
@JsonProperty("quality")
private float quality;
private Float quality;
public Achievement(int id, int gameId, String name, String description, int stages, float completion, float difficulty, float quality) {
this.id = id;
this.gameId = gameId;
this.name = name;
this.description = description;
this.stages = stages;
this.completion = completion;
this.difficulty = difficulty;
this.quality = quality;
public int getID() {
return ID;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public int getGameId() {
return gameId;
public void setID(int ID) {
this.ID = ID;
}
public void setGameId(int gameId) {
this.gameId = gameId;
public String getGame() {
return game;
}
public void setGame(String game) {
this.game = game;
}
public String getName() { return name; }
@ -127,31 +43,27 @@ public class Achievement {
public void setDescription(String description) { this.description = description; }
public int getStages() { return stages; }
public void setStages(int stages) { this.stages = stages; }
public float getCompletion() {
public Integer getCompletion() {
return completion;
}
public void setCompletion(float completion) {
public void setCompletion(Integer completion) {
this.completion = completion;
}
public float getDifficulty() {
public Float getDifficulty() {
return difficulty;
}
public void setDifficulty(float difficulty) {
public void setDifficulty(Float difficulty) {
this.difficulty = difficulty;
}
public float getQuality() {
public Float getQuality() {
return quality;
}
public void setQuality(float quality) {
public void setQuality(Float quality) {
this.quality = quality;
}
}

View file

@ -1,20 +1,11 @@
package achievements.data;
import achievements.data.query.StringFilter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
public class Game {
public static class Query {
@JsonProperty("name")
private StringFilter name;
@JsonProperty("platforms")
private StringFilter platforms;
}
@JsonProperty("ID")
private int id;
@JsonProperty("name")
@ -24,13 +15,6 @@ public class Game {
@JsonProperty("achievementCount")
private int achievementCount;
public Game(int id, String name, String platform) {
this.id = id;
this.name = name;
this.platforms = new ArrayList<>();
this.platforms.add(platform);
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }

View file

@ -1,6 +1,5 @@
package achievements.data;
import achievements.data.query.StringFilter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

View file

@ -11,13 +11,16 @@ public class Session {
private int id;
@JsonProperty("hue")
private int hue;
@JsonProperty("admin")
private boolean admin;
@JsonIgnore
private boolean used;
public Session(String key, int id, int hue) {
public Session(String key, int id, int hue, boolean admin) {
this.key = key;
this.id = id;
this.hue = hue;
this.admin = admin;
this.used = false;
}
@ -45,7 +48,15 @@ public class Session {
this.hue = hue;
}
public boolean getUsed() {
public boolean isAdmin() {
return admin;
}
public void setAdmin(boolean admin) {
this.admin = admin;
}
public boolean isUsed() {
return used;
}

View file

@ -2,7 +2,7 @@ package achievements.data.query;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AddPlatformRequest {
public class AddPlatform {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("platformId")

View file

@ -1,32 +0,0 @@
package achievements.data.query;
import com.fasterxml.jackson.annotation.JsonProperty;
public class NumericFilter {
@JsonProperty("min")
private Float min;
@JsonProperty("max")
private Float max;
public NumericFilter(Float min, Float max) {
this.min = min;
this.max = max;
}
public Float getMin() {
return min;
}
public void setMin(Float min) {
this.min = min;
}
public Float getMax() {
return max;
}
public void setMax(Float max) {
this.max = max;
}
}

View file

@ -2,7 +2,7 @@ package achievements.data.query;
import com.fasterxml.jackson.annotation.JsonProperty;
public class RemovePlatformRequest {
public class RemovePlatform {
@JsonProperty("sessionKey")
private String sessionKey;
@JsonProperty("platformId")

View file

@ -0,0 +1,97 @@
package achievements.data.query;
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;
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;
}
}

View file

@ -1,21 +0,0 @@
package achievements.data.query;
import com.fasterxml.jackson.annotation.JsonProperty;
public class StringFilter {
@JsonProperty("query")
private String query;
public StringFilter(String query) {
this.query = query;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
}

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

@ -13,12 +13,12 @@ public class SessionManager {
private HashMap<String, Session> sessions;
public SessionManager() {
sessions = new HashMap();
sessions = new HashMap<>();
}
public Session generate(int user, int hue) {
public Session generate(int user, int hue, boolean admin) {
var key = HashManager.encode(HashManager.generateBytes(16));
var session = new Session(key, user, hue);
var session = new Session(key, user, hue, admin);
sessions.put(key, session);
return session;
}
@ -32,8 +32,13 @@ public class SessionManager {
}
public boolean validate(int user, String key) {
var foreign = sessions.get(key);
return foreign != null && user == foreign.getId();
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) {
@ -51,7 +56,7 @@ public class SessionManager {
public void clean() {
var remove = new ArrayList<String>();
sessions.forEach((key, session) -> {
if (!session.getUsed()) {
if (!session.isUsed()) {
remove.add(session.getKey());
} else {
session.setUsed(false);

View file

@ -0,0 +1,111 @@
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;
@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 AddGameToPlatform(?, ?, ?)}");
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, platformUserId);
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();
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);
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,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 AchievementService {
@Autowired
private DbConnection dbs;
private Connection db;
@Autowired
private ImageService imageService;
@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;
}
}

View file

@ -68,7 +68,8 @@ public class AuthenticationService {
statement.getInt(1),
session.generate(
statement.getInt(6),
statement.getInt(7)
statement.getInt(7),
false
)
);
statement.close();
@ -95,7 +96,8 @@ public class AuthenticationService {
0,
session.generate(
result.getInt("ID"),
result.getInt("Hue")
result.getInt("Hue"),
result.getBoolean("Admin")
)
);
} else {

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 GetAchievementIcon(?)}");
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

@ -14,8 +14,21 @@ public class PlatformService {
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,59 @@
package achievements.services;
import achievements.data.Achievement;
import achievements.data.query.SearchAchievements;
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.ArrayList;
import java.util.List;
@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); }
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;
}
}

View file

@ -1,19 +1,24 @@
package achievements.services;
import achievements.data.Profile;
import achievements.data.query.AddPlatformRequest;
import achievements.data.query.RemovePlatformRequest;
import achievements.data.query.AddPlatform;
import achievements.data.query.RemovePlatform;
import achievements.data.query.SetUsername;
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.HashMap;
import static achievements.services.ImageService.MIME_TO_EXT;
@Service
public class UserService {
@ -25,6 +30,12 @@ public class UserService {
@Autowired
private AuthenticationService auth;
@Autowired
private APIService apiService;
@Autowired
private ImageService imageService;
@PostConstruct
private void init() {
db = dbs.getConnection();
@ -97,55 +108,51 @@ public class UserService {
return -1;
}
private static final HashMap<String, String> VALID_IMAGE_TYPES = new HashMap<>();
static {
VALID_IMAGE_TYPES.put("apng", "apng");
VALID_IMAGE_TYPES.put("avif", "avif");
VALID_IMAGE_TYPES.put("gif", "gif" );
VALID_IMAGE_TYPES.put("jpeg", "jpg" );
VALID_IMAGE_TYPES.put("png", "png" );
VALID_IMAGE_TYPES.put("svg+xml", "svg" );
VALID_IMAGE_TYPES.put("webp", "webp");
}
public String[] getProfileImageType(int userId) {
public String[] getProfileImage(int userId) {
try {
var stmt = db.prepareCall("{call GetUserImage(?)}");
stmt.setInt(1, userId);
var result = stmt.executeQuery();
if (result.next()) {
var type = result.getString("PFP");
if (type == null) {
return new String[] { "default", "png", "png" };
} else {
return new String[] { Integer.toString(userId), VALID_IMAGE_TYPES.get(type), type };
}
}
} catch (SQLException e) {
e.printStackTrace();
} catch (NumberFormatException e) {
return imageService.getImageType(stmt, userId);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String setProfileImageType(int userId, String sessionKey, String type) {
public String setProfileImage(int userId, String sessionKey, MultipartFile file) {
try {
var type = file.getContentType();
if (type.matches("image/.*")) {
type = type.substring(6);
var extension = VALID_IMAGE_TYPES.get(type);
type = MIME_TO_EXT.get(type);
if (!auth.session().validate(userId, sessionKey)) {
return "forbidden";
} else if (extension == null) {
} else if (type == null) {
return "unsupported_type";
} else {
var stmt = db.prepareCall("{call SetUserImage(?, ?)}");
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);
return extension;
// 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";
@ -156,28 +163,41 @@ public class UserService {
return "unknown";
}
public int addPlatform(int userId, AddPlatformRequest request) {
try {
public int addPlatform(int userId, AddPlatform request) {
if (auth.session().validate(userId, request.getSessionKey())) {
var stmt = db.prepareCall("{call AddPlatform(?, ?, ?)}");
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, RemovePlatformRequest request) {
public int removePlatform(int userId, RemovePlatform request) {
try {
if (auth.session().validate(userId, request.getSessionKey())) {
var stmt = db.prepareCall("{call RemovePlatform(?, ?)}");
var stmt = db.prepareCall("{call RemoveUserFromPlatform(?, ?)}");
stmt.setInt(1, userId);
stmt.setInt(2, request.getPlatformId());

View file

@ -30,10 +30,16 @@ app.get("/login", (req, res) => {
res.sendFile(path.join(__dirname + "/webpage/login.html"));
});
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname + "/webpage/index.html"));
res.sendFile(path.join(__dirname + "/webpage/search_achievements.html"));
});
app.get("/about", (req, res) => {
res.sendFile(path.join(__dirname + "/webpage/about.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("/profile/:id", (req, res) => {
res.sendFile(path.join(__dirname + "/webpage/profile.html"));

View file

@ -1,42 +0,0 @@
<!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/about.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="about-page" class="page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">About</p>
<div class="page-header-separator"></div>
</div>
</div>
<div class="page-subsection">
<div class="page-subsection-wrapper">
<p id="about-text" class="page-subsection-chunk">Collate achievement data from multiple platforms into a single location. Explore achievement data of yourself and others.</p>
</div>
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/about.js"></script>
</body>
</html>

View file

@ -28,6 +28,10 @@
<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="profile-page">
<div id="profile-section-1">
<div id="profile-info" class="page-subsection">
@ -49,7 +53,7 @@
</div>
<div id="profile-info-pfp-border" class="page-subsection-chunk">
<div id="profile-info-pfp">
<img id="profile-info-pfp-img" src="/api/user/${id}/image" alt="User's Profile Picture" />
<img id="profile-info-pfp-img" class="lazy-img" data-src="/api/user/${id}/image" alt="User's Profile Picture" />
<div id="profile-info-pfp-vignette"></div>
<img id="profile-info-pfp-upload" src="/static/res/upload.svg" alt="Upload Image" />
<img id="profile-info-pfp-upload-hover" src="/static/res/upload-hover.svg" alt="Upload Image" />

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">Search Achievements</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="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 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="completed-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Completed</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 class="list-page-entry-text achievement-game-name">Game</p>
<p class="list-page-entry-text achievement-name">Name</p>
<p class="list-page-entry-text achievement-completion">Completion Rate</p>
<p class="list-page-entry-text achievement-difficulty">Difficulty</p>
<p 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">
<img class="list-page-entry-icon lazy-img" data-src="/api/achievement/${achievement_id}/image" alt="Achievement Thumbnail"></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

@ -6,7 +6,8 @@
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/index.css" />
<link rel="stylesheet" href="/static/styles/search.css" />
<link rel="stylesheet" href="/static/styles/search_games.css" />
</head>
<body>
<div id="navbar">
@ -21,10 +22,10 @@
</template>
</div>
<div id="content-body">
<div id="index-page" class="page">
<div id="search-games-page" class="search page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Achievements Project</p>
<p class="page-header-text">Search Games</p>
<div class="page-header-separator"></div>
</div>
</div>
@ -126,6 +127,7 @@
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/index.js"></script>
<script src="/static/scripts/search.js"></script>
<script src="/static/scripts/search_games.js"></script>
</body>
</html>

View file

@ -0,0 +1,133 @@
<!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">Search Users</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 for="achievement-search">Search</label>
<input id="achievement-search" type="text" placeholder="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 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 id="games-owned-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Games Owned</p>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">General</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div id="from-games-owned-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">From My Games</p>
</div>
<div id="completed-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Completed</p>
</div>
<div id="completed-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Completed</p>
</div>
</div>
</div>
</div>
<div class="list-page-filter-section page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Platforms</p>
<div class="page-subheader-separator"></div>
</div>
<div class="list-page-filter-chunk page-subsection-chunk">
<div class="page-subsection-wrapper">
<div id="games-owned-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Games Owned</p>
</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 achievement-name">Name</p>
<p class="list-page-entry-text achievement-description">Description</p>
<p class="list-page-entry-text achievement-stages">Stages</p>
</div>
<template data-template="achievements-page-list: List<Basic>">
<div class="list-page-entry">
<img class="list-page-entry-icon" src="/static/res/dummy_achievement.png" alt="Achievement Thumbnail"></img>
<div class="list-page-entry-text-section">
<p class="list-page-entry-text achievement-name">${achievement-name}</p>
<p class="list-page-entry-text achievement-description">${achievement-description}</p>
<p class="list-page-entry-text achievement-stages">${stages}</p>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</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 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

@ -25,7 +25,6 @@ const loadSession = async () => {
await fetch(`/api/auth/refresh`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
@ -52,8 +51,10 @@ const commonTemplates = async () => {
{ section: "right" }
]);
template.apply("navbar-section-left").values([
{ item: "project", title: "Project" },
{ item: "about", title: "About" }
{ item: "achievements", title: "Achievements" },
{ item: "users", title: "Users" },
{ item: "games", title: "Games" },
{ item: "import", title: "Import" }
]);
if (session) {
template.apply("navbar-section-right").values([
@ -67,29 +68,35 @@ const commonTemplates = async () => {
}
};
const loadLazyImages = () => {
const imgs = document.querySelectorAll(".lazy-img");
for (const img of imgs) {
img.src = img.dataset.src;
}
}
const connectNavbar = () => {
const navItems = document.querySelectorAll(".navbar-item");
if (!session || !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',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
})
.then(response => {
});
session = undefined;
window.location.href = "/login";
});
});
} else if (item.dataset.pageName === "profile") {
item.addEventListener("click", (clickEvent) => window.location.href = `/profile/${session.id}`);
} else if (item.dataset.pageName === "project") {
item.addEventListener("click", (clickEvent) => window.location.href = `/`);
} else {
item.addEventListener("click", (clickEvent) => window.location.href = `/${item.dataset.pageName}`);
}

View file

@ -1,24 +0,0 @@
const expandTemplates = async () => {
await commonTemplates();
}
const loadFilters = () => {
const filtersButton = document.querySelector("#filter-dropdown-stack");
const filters = document.querySelector("#list-page-filters-flex");
filtersButton.addEventListener("click", (clickEvent) => {
filtersButton.classList.toggle("active");
filters.classList.toggle("active");
});
}
window.addEventListener("load", async (loadEvent) => {
loadRoot();
loadSession();
await expandTemplates();
await template.expand();
connectNavbar();
loadFilters();
});

View file

@ -11,6 +11,7 @@ window.addEventListener("load", async (loadEvent) => {
password: document.querySelector("#password"),
confirm: document.querySelector("#confirm" )
};
fields.email.focus();
const createUser = document.querySelector("#create-user-button");
const login = document.querySelector("#login-button");
@ -80,7 +81,6 @@ window.addEventListener("load", async (loadEvent) => {
freeze();
fetch(`/api/auth/create_user`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
@ -141,7 +141,6 @@ window.addEventListener("load", async (loadEvent) => {
freeze();
fetch(`/api/auth/login`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},

View file

@ -2,14 +2,25 @@ let profileId = window.location.pathname.split('/').pop();
let isReturn = false;
let profileData = null;
const loadProfile = () => {
{
const lists = document.querySelectorAll(".profile-list");
const checkLists = () => {
for (const list of lists) {
if (list.querySelectorAll(".profile-entry").length === 0) {
list.parentElement.removeChild(list);
let found = false;
const entries = list.querySelectorAll(".profile-entry");
for (const entry of entries) {
if (window.getComputedStyle(entry).getPropertyValue('display') !== 'none') {
found = true;
break;
}
}
if (!found) {
list.style.display = 'none';
} else {
list.style.display = 'block';
}
}
}
checkLists();
{
const validImageFile = (type) => {
@ -32,7 +43,6 @@ const loadProfile = () => {
if (usernameField.value !== '') {
fetch(`/api/user/${profileId}/username`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
@ -89,7 +99,6 @@ const loadProfile = () => {
fetch(`/api/user/${profileId}/image`, {
method: 'POST',
mode: 'cors',
body: data
}).then(response => {
if (upload.classList.contains("active")) {
@ -141,6 +150,7 @@ const loadProfile = () => {
for (const platform of platforms) {
platform.classList.toggle("editing");
}
checkLists();
};
editPlatformsButton.addEventListener("click", togglePlatformEdit);
savePlatformsButton.addEventListener("click", togglePlatformEdit);
@ -156,7 +166,6 @@ const loadProfile = () => {
steamButtons[1].addEventListener("click", (clickEvent) => {
fetch(`/api/user/${profileId}/platforms/remove`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
@ -213,11 +222,11 @@ const expandTemplates = async () => {
template.apply("profile-platforms-list").promise(profileData.then(data =>
data.platforms.map(platform => ({
platform_id: platform.id,
img: `<img class="profile-entry-icon" src="/api/platform/image/${platform.id}" alt="Steam Logo" />`,
img: `<img class="profile-entry-icon" src="/api/platform/${platform.id}/image" alt="Steam Logo" />`,
name: platform.name,
connected: platform.connected ? "connected" : "",
add:
(platform.id === 0 ? `<img id="add-steam" class="platform-add" src="https://community.cloudflare.steamstatic.com/public/images/signinthroughsteam/sits_01.png" alt="Add" />` :
(platform.id === 0 ? `<img id="add-steam" class="platform-add" src="https://steamcdn-a.akamaihd.net/steamcommunity/public/images/steamworks_docs/english/sits_small.png" alt="Add" />` :
(platform.id === 1 ? `<p class="platform-unsupported">Coming soon...</p>` :
(platform.id === 2 ? `<p class="platform-unsupported">Coming soon...</p>` :
"")))
@ -228,6 +237,7 @@ const expandTemplates = async () => {
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
var importing = document.querySelector("#importing");
if (!/\d+/.test(profileId)) {
isReturn = true;
const platform = profileId;
@ -238,6 +248,9 @@ window.addEventListener("load", async (loadEvent) => {
delete session.lastProfile;
}
const importingText = importing.querySelector("#importing-text");
importingText.textContent = `Importing from ${platform}...`;
importing.style.display = `flex`;
if (platform === 'steam') {
const query = new URLSearchParams(window.location.search);
@ -246,9 +259,8 @@ window.addEventListener("load", async (loadEvent) => {
} else {
// Regex courtesy of https://github.com/liamcurry/passport-steam/blob/master/lib/passport-steam/strategy.js
var steamId = /^https?:\/\/steamcommunity\.com\/openid\/id\/(\d+)$/.exec(query.get('openid.claimed_id'))[1];
await fetch("/api/user/platforms/add", {
await fetch(`/api/user/${profileId}/platforms/add`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
@ -266,13 +278,15 @@ window.addEventListener("load", async (loadEvent) => {
} else {
// Handle error
}
importing.remove();
profileData = fetch(`/api/user/${profileId}`, { method: 'GET', mode: 'cors' })
profileData = fetch(`/api/user/${profileId}`, { method: 'GET' })
.then(response => response.json());
await expandTemplates();
await template.expand();
loadLazyImages();
connectNavbar();
loadProfile();
});

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,108 @@
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 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 canSearch = true;
const loadList = async () => {
if (canSearch) {
canSearch = false;
const body = {
searchTerm: searchField.value,
userId: completed.classList.contains('active') ? session.id : null,
completed: completed.classList.contains('active'),
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 ),
};
console.log(body);
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();
});
}
};
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

@ -136,6 +136,10 @@ var template = template || {};
}
};
template.clear = () => {
templateEntryMap.clear();
}
const parseType = (type) => {
let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/);
let id = result[1];

View file

@ -70,6 +70,15 @@ html, body {
background-color: var(--accent-value3);
}
@keyframes load {
from { transform: rotateZ(0deg ); }
to { transform: rotateZ(360deg); }
}
.ap-loading {
animation: 1.5s cubic-bezier(0.4, 0.15, 0.6, 0.85) 0s infinite running load;
}
.ap-button {
color: var(--foreground);
background-color: var(--accent-value2);
@ -226,201 +235,3 @@ html, body {
background-color: var(--accent-value3);
}
.list-page-search {
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.list-page-search > label,
.list-page-search > input {
box-sizing: border-box;
padding: 12px 20px;
color: var(--foreground);
font-size: 24px;
}
.list-page-search > label {
background-color: var(--accent-value2);
}
.list-page-search > label:hover {
background-color: var(--accent-value3);
}
.list-page-search > label:active {
background-color: var(--accent-value1);
transition-property: background-color;
transition-duration: 0.15s;
}
.list-page-search > input {
background-color: var(--distinction);
border: 0;
flex-grow: 1;
outline: none;
transition-property: background-color, color;
transition-duration: 0.075s;
}
.list-page-search > input:focus {
background-color: var(--foreground);
color: var(--background);
}
.list-page-partitions {
box-sizing: border-box;
width: 100%;
height: max-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
.list-page-filter-partition {
width: 20%;
max-width: 640px;
}
.list-page-filter-chunk {
background-color: var(--distinction);
width: 100%;
height: 100%;
}
.list-page-filter {
box-sizing: border-box;
width: 100%;
height: max-content;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.list-page-filter-checkbox {
width: 28px;
height: 28px;
background-color: var(--foreground);
border: 3px solid var(--foreground);
border-radius: 8px;
transition-property: background-color, border-color;
transition-duration: 0.15s;
}
.list-page-filter:hover > .list-page-filter-checkbox {
background-color: var(--foreground);
border-color: var(--selected-accent1);
}
.list-page-filter.selected > .list-page-filter-checkbox {
background-color: var(--selected-accent1);
border-color: var(--selected-accent1);
}
.list-page-filter.selected:hover > .list-page-filter-checkbox {
background-color: var(--selected-accent0);
border-color: var(--selected-accent1);
}
.list-page-filter-name {
margin: 0;
padding: 16px;
color: var(--foreground);
font-size: 24px;
user-select: none;
}
.list-page-list-partition {
box-sizing: border-box;
flex-grow: 1;
}
.list-page-list {
border-radius: 8px;
overflow: hidden;
}
.list-page-header {
width: 100%;
height: 64px;
background-color: var(--accent-value2);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--foreground);
font-size: 24px;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-page-entry {
width: 100%;
height: 64px;
background-color: var(--distinction);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--foreground);
font-size: 24px;
}
.list-page-entry-icon {
width: 64px;
height: 64px;
flex-grow: 0;
}
.list-page-entry-text {
box-sizing: border-box;
margin: 0;
padding: 0 12px;
height: 64px;
line-height: 64px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-top: 1px solid var(--background);
}
.list-page-header > .list-page-entry-text {
border: 0;
}

View file

@ -1,111 +0,0 @@
#index-page {
max-width: 1600px;
}
#list-page-search-filters {
width: 100%;
height: max-content;
}
#list-page-search-dropdown {
display: flex;
flex-direction: row;
align-items: center;
}
#search-wrapper {
width: 100%;
}
#list-page-search-pair {
flex-grow: 1;
}
#filter-dropdown-wrapper {
box-sizing: border-box;
height: 84px;
width: 84px;
}
#filter-dropdown-stack {
width: 100%;
height: 100%;
position: relative;
}
#filter-dropdown-stack.active {
transform: rotateZ(-90deg);
}
#filter-dropdown-button {
position: absolute;
left: 0;
top: 0;
height: 100%;
display: block;
}
#filter-dropdown-stack:hover > #filter-dropdown-button {
display: none;
}
#filter-dropdown-button-hover {
position: absolute;
left: 0;
top: 0;
height: 100%;
display: none;
}
#filter-dropdown-stack:hover > #filter-dropdown-button-hover {
display: block;
}
#list-page-filters-flex {
display: none;
width: 100%;
height: max-content;
flex-direction: row;
}
#list-page-filters-flex.active {
display: flex;
}
.list-page-filter-section {
box-sizing: border-box;
flex-basis: 0;
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
}
#list-page-filters-background {
background-color: var(--distinction);
}
.list-page-entry-text.achievement-name {
flex-grow: 3;
flex-basis: 0;
}
.list-page-entry-text.achievement-description {
flex-grow: 6;
flex-basis: 0;
}
.list-page-entry-text.achievement-stages {
flex-grow: 1;
flex-basis: 0;
}

View file

@ -2,8 +2,6 @@
--form-spacing: 48px;
--element-spacing: 12px;
--error: #F95959;
}
#login-page {

View file

@ -2,6 +2,26 @@
max-width: 1600px;
}
#importing {
flex-direction: column;
align-items: center;
display: none;
}
#importing-text {
margin: 0;
height: 96px;
font-size: 64px;
line-height: 96px;
color: var(--foreground);
}
#importing-loading {
height: 64px;
width: 64px;
}
.profile-list {
width: 100%;
height: max-content;
@ -150,7 +170,7 @@
border-radius: 8px;
object-fit: contain;
background-color: var(--background);
background-color: var(--background-dark);
position: absolute;
}
@ -178,7 +198,7 @@
border-radius: 8px;
background-color: var(--background);
background-color: var(--background-dark);
opacity: 0.8;
display: block;

View file

@ -0,0 +1,330 @@
#list-page-search-filters {
width: 100%;
height: max-content;
}
#list-page-search-dropdown {
display: flex;
flex-direction: row;
align-items: center;
}
#search-wrapper {
width: 100%;
}
#list-page-search-pair {
flex-grow: 1;
}
#filter-dropdown-wrapper {
box-sizing: border-box;
height: 84px;
width: 84px;
}
#filter-dropdown-stack {
width: 100%;
height: 100%;
position: relative;
}
#filter-dropdown-stack.active {
transform: rotateZ(-90deg);
}
#filter-dropdown-button {
position: absolute;
left: 0;
top: 0;
height: 100%;
display: block;
}
#filter-dropdown-stack:hover > #filter-dropdown-button {
display: none;
}
#filter-dropdown-button-hover {
position: absolute;
left: 0;
top: 0;
height: 100%;
display: none;
}
#filter-dropdown-stack:hover > #filter-dropdown-button-hover {
display: block;
}
#list-page-filters-flex {
display: none;
width: 100%;
height: max-content;
flex-direction: row;
}
#list-page-filters-flex.active {
display: flex;
}
.list-page-filter-section {
box-sizing: border-box;
flex-basis: max-content;
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
}
.list-page-filter-partition {
width: 20%;
max-width: 640px;
}
.list-page-filter-chunk {
background-color: var(--distinction);
width: 100%;
height: 100%;
}
.list-page-filter {
box-sizing: border-box;
width: 100%;
height: max-content;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.list-page-filter-checkbox {
width: 28px;
height: 28px;
background-color: var(--foreground);
border: 3px solid var(--foreground);
border-radius: 8px;
transition-property: background-color, border-color;
transition-duration: 0.15s;
}
.list-page-filter:hover > .list-page-filter-checkbox {
background-color: var(--foreground);
border-color: var(--selected-accent1);
}
.list-page-filter.selected > .list-page-filter-checkbox {
background-color: var(--selected-accent1);
border-color: var(--selected-accent1);
}
.list-page-filter.selected:hover > .list-page-filter-checkbox {
background-color: var(--selected-accent0);
border-color: var(--selected-accent1);
}
.list-page-filter-name,
.list-page-filter-label {
margin: 0;
padding: 16px;
color: var(--foreground);
font-size: 24px;
user-select: none;
}
.list-page-filter-label {
width: 40%;
}
.list-page-filter-param {
padding: 4px;
width: 25%;
font-size: 24px;
color: var(--background);
background-color: var(--foreground);
border-radius: 8px;
border: 0;
outline: none;
}
#list-page-filters-background {
background-color: var(--distinction);
}
.list-page-entry-text {
flex-basis: 0;
}
.page.search {
max-width: 1720px;
}
.list-page-search {
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.list-page-search > label,
.list-page-search > input {
box-sizing: border-box;
padding: 12px 20px;
color: var(--foreground);
font-size: 24px;
}
.list-page-search > label {
background-color: var(--accent-value2);
}
.list-page-search > label:hover {
background-color: var(--accent-value3);
}
.list-page-search > label:active {
background-color: var(--accent-value1);
transition-property: background-color;
transition-duration: 0.15s;
}
.list-page-search > input {
background-color: var(--distinction);
border: 0;
flex-grow: 1;
outline: none;
transition-property: background-color, color;
transition-duration: 0.075s;
}
.list-page-search > input:focus {
background-color: var(--foreground);
color: var(--background);
}
.list-page-partitions {
box-sizing: border-box;
width: 100%;
height: max-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
.list-page-list-partition {
box-sizing: border-box;
flex-grow: 1;
}
.list-page-list {
border-radius: 8px;
overflow: hidden;
}
.list-page-header {
width: 100%;
height: 64px;
background-color: var(--accent-value2);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--foreground);
font-size: 24px;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-page-entry {
width: 100%;
height: 64px;
background-color: var(--distinction);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--foreground);
font-size: 24px;
}
.list-page-entry-icon {
width: 64px;
height: 64px;
flex-grow: 0;
}
.list-page-entry-text {
box-sizing: border-box;
margin: 0;
padding: 0 12px;
height: 64px;
line-height: 64px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-page-entry > .list-page-entry-text {
border-top: 1px solid var(--background);
border-left: 1px solid var(--background);
}
.list-page-header > .list-page-entry-text {
border-left: 1px solid var(--accent-value0);
}
#loading-results {
margin: 16px 0;
width: 100%;
height: 64px;
object-fit: contain;
display: none;
}

View file

@ -0,0 +1,5 @@
.list-page-entry-text.achievement-game-name { flex-grow: 1.75; }
.list-page-entry-text.achievement-name { flex-grow: 2; }
.list-page-entry-text.achievement-completion { flex-grow: 1; }
.list-page-entry-text.achievement-quality { flex-grow: 1; }
.list-page-entry-text.achievement-difficulty { flex-grow: 1; }

View file

@ -18,4 +18,6 @@
--selected-accent0: #2266CC;
--selected-accent1: #3388FF;
--error: #F95959;
}

Binary file not shown.

639
sql/DataProcs.sql Normal file
View file

@ -0,0 +1,639 @@
---------------------------------------
-- GET USER NAME AND STATS PROCEDURE --
---------------------------------------
CREATE PROCEDURE GetUserNameAndStats(
@userId INT,
@username VARCHAR(32) OUTPUT,
@completed INT OUTPUT,
@average INT OUTPUT,
@perfect INT OUTPUT
)
AS
SELECT @username = Username
FROM [User]
WHERE ID = @userId
IF @username IS NULL
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
SELECT @completed = SUM(Completed)
FROM GameCompletionByUser
WHERE UserID = @userId
SELECT @average = AVG((Completed * 100) / Total)
FROM GameCompletionByUser
WHERE UserID = @userId
SELECT @perfect = COUNT(GameID)
FROM GameCompletionByUser
WHERE UserID = @userId AND Completed = Total
RETURN 0
GO
SELECT * FROM [User]
----------------------------------
-- GET USER PLATFORMS PROCEDURE --
----------------------------------
CREATE PROCEDURE GetUserPlatforms(
@userId INT
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
SELECT [Platform].ID, [PlatformName], (CASE WHEN UserID IS NOT NULL THEN 1 ELSE 0 END) AS Connected
FROM [Platform]
LEFT JOIN IsOn ON IsOn.PlatformID = [Platform].ID AND UserID = @userId
ORDER BY [Platform].ID
RETURN 0
GO
--------------------------------
-- GET USER RATINGS PROCEDURE --
--------------------------------
CREATE PROCEDURE GetUserRatings(
@userId INT
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
SELECT Game.Name AS GameName, Achievement.Name AS AchievementName, Quality, Difficulty, Rating.[Description]
FROM Rating
JOIN Achievement ON Achievement.ID = Rating.AchievementID
JOIN Game ON Game.ID = Achievement.GameID
WHERE UserID = @userId
RETURN 0
GO
------------------------------
-- GET USER IMAGE PROCEDURE --
------------------------------
CREATE PROCEDURE GetUserImage(
@userId INT
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
SELECT ProfileImage FROM [User] WHERE ID = @userId
RETURN 0
GO
------------------
-- SET USERNAME --
------------------
CREATE PROCEDURE SetUsername(
@userId INT,
@username VARCHAR(32)
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
UPDATE [User] SET Username = @username WHERE ID = @userId
RETURN 0
GO
------------------------------
-- SET USER IMAGE PROCEDURE --
------------------------------
CREATE PROCEDURE SetUserImage(
@userId INT,
@type ImageType,
@oldType ImageType OUTPUT
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
SELECT @oldType = ProfileImage FROM [User] WHERE ID = @userId
UPDATE [User] SET ProfileImage = @type WHERE ID = @userId
RETURN 0
GO
--------------------------
-- ADD USER TO PLATFORM --
--------------------------
CREATE PROCEDURE AddUserToPlatform(
@userId INT,
@platformId INT,
@platformUserID VARCHAR(32)
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 2
END
IF EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'User already exists on specified platform'
RETURN 3
END
INSERT INTO IsOn VALUES (@userId, @platformId, @platformUserId)
RETURN 0
GO
-------------------------------
-- REMOVE USER FROM PLATFORM --
-------------------------------
CREATE PROCEDURE RemoveUserFromPlatform(
@userId INT,
@platformId INT
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 2
END
IF NOT EXISTS (SELECT UserID FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'User does not exist on specified platform'
RETURN 3
END
DELETE FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId
DELETE FROM Progress WHERE UserID = @userId AND PlatformID = @platformId
DELETE FROM Owns WHERE UserID = @userId AND PlatformID = @platformId
RETURN 0
GO
------------------
-- ADD PLATFORM --
------------------
CREATE PROCEDURE AddPlatform(
@name VARCHAR(32),
@platformId INT OUTPUT
)
AS
IF @name IS NULL
BEGIN
PRINT 'Platform name cannot be null'
RETURN 1
END
INSERT INTO [Platform] VALUES (@name)
SET @platformId = @@IDENTITY
RETURN 0
GO
---------------------
-- REMOVE PLATFORM --
---------------------
CREATE PROCEDURE RemovePlatform(
@platformId INT
)
AS
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 1
END
IF @platformId IN (SELECT PlatformID FROM ExistsOn)
BEGIN
PRINT 'All games must be removed from the specified platform before it can be removed'
RETURN 2
END
DELETE FROM [Platform] WHERE ID = @platformId
RETURN 0
GO
-------------------
-- GET PLATFORMS --
-------------------
CREATE PROCEDURE GetPlatforms
AS
SELECT ID, PlatformName FROM [Platform]
RETURN 0
GO
-----------------------
-- GET PLATFORM NAME --
-----------------------
CREATE PROCEDURE GetPlatformName(
@platformId INT,
@name VARCHAR(32) OUTPUT
)
AS
SELECT @name = PlatformName FROM [Platform] WHERE ID = @platformId
IF @name IS NULL
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 1
END
RETURN 0
GO
-----------------------
-- GET PLATFORM ICON --
-----------------------
CREATE PROCEDURE GetPlatformIcon(
@platformId INT
)
AS
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 1
END
SELECT Icon FROM [Platform] WHERE ID = @platformId
RETURN 0
GO
--------------
-- ADD GAME --
--------------
CREATE PROCEDURE AddGame(
@name VARCHAR(32),
@image ImageType,
@gameId INT OUTPUT
)
AS
IF @name IS NULL
BEGIN
PRINT 'Game name cannot be null'
RETURN 1
END
IF @name IN (SELECT [Name] FROM Game)
BEGIN
PRINT 'Game with specified name already exists'
RETURN 2
END
INSERT INTO Game VALUES (@name, @image)
SET @gameId = @@IDENTITY
RETURN 0
GO
---------------------
-- ADD IF NOT GAME --
---------------------
CREATE PROCEDURE AddIfNotGame(
@name VARCHAR(32),
@image VARCHAR(11),
@gameId INT OUTPUT
)
AS
IF @name IS NULL
BEGIN
PRINT 'Game name cannot be null'
RETURN 1
END
-- Ideally game name wouldn't have to be unique, but I don't know of another way to sync games across platforms when they share no IDing system
IF NOT @name IN (SELECT [Name] FROM Game)
BEGIN
INSERT INTO Game VALUES (@name, @image)
END
SELECT @gameId = ID FROM Game WHERE [Name] = @name
RETURN 0
GO
-----------------
-- REMOVE GAME --
-----------------
CREATE PROCEDURE RemoveGame(
@gameId INT
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
DELETE FROM Game WHERE ID = @gameId
RETURN 0
GO
-------------------
-- GET GAME ICON --
-------------------
CREATE PROCEDURE GetGameIcon(
@gameId INT
)
AS
IF NOT @gameId IN (SELECT ID FROM [Game])
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
SELECT Icon FROM [Game] WHERE ID = @gameId
RETURN 0
GO
----------------------
-- ADD GAME TO USER --
----------------------
CREATE PROCEDURE AddGameToUser(
@gameId INT,
@userId INT,
@platformId INT
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 2
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 3
END
IF NOT EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'User is not on specified platform'
RETURN 4
END
IF EXISTS (SELECT * FROM Owns WHERE GameID = @gameId AND UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'Game is already owned by specified user on specified platform'
RETURN 5
END
INSERT INTO Owns VALUES (@userId, @gameId, @platformId)
RETURN 0
GO
---------------------------
-- REMOVE GAME FROM USER --
---------------------------
CREATE PROCEDURE RemoveGameFromUser(
@gameId INT,
@userId INT,
@platformId INT
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 2
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 3
END
IF NOT EXISTS (SELECT * FROM Owns WHERE GameID = @gameId AND UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'Game is not owned by specified user on specified platform'
RETURN 4
END
DELETE FROM Owns WHERE UserID = @userId AND GameID = @gameId AND PlatformID = @platformId
RETURN 0
GO
--------------------------
-- ADD GAME TO PLATFORM --
--------------------------
CREATE PROCEDURE AddGameToPlatform(
@gameId INT,
@platformId INT,
@platformGameId VARCHAR(32)
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 2
END
IF EXISTS (SELECT * FROM ExistsOn WHERE GameID = @gameId AND PlatformID = @platformId)
BEGIN
PRINT 'Game already exists on specified platform'
RETURN 3
END
INSERT INTO ExistsOn VALUES (@gameId, @platformId, @platformGameId)
RETURN 0
GO
-------------------------------
-- REMOVE GAME FROM PLATFORM --
-------------------------------
CREATE PROCEDURE RemoveGameFromPlatform(
@gameId INT,
@platformId INT
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 2
END
IF NOT EXISTS (SELECT * FROM ExistsOn WHERE GameID = @gameId AND PlatformID = @platformId)
BEGIN
PRINT 'Game does not exist on specified platform'
RETURN 3
END
DELETE FROM ExistsOn WHERE GameID = @gameId AND PlatformID = @platformId
RETURN 0
GO
---------------------
-- ADD ACHIEVEMENT --
---------------------
CREATE PROCEDURE AddAchievement(
@gameId INT,
@name VARCHAR(128),
@description VARCHAR(512),
@stages INT,
@image ImageType,
@achievementId INT OUTPUT
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
IF @name IS NULL
BEGIN
PRINT 'Achievement name cannot be null'
RETURN 2
END
IF @stages IS NULL
BEGIN
PRINT 'Achievement stages cannot be null'
RETURN 3
END
IF @name IN (SELECT [Name] FROM Achievement WHERE GameID = @gameId)
BEGIN
PRINT 'Achievement with specified name already exists for specified game'
RETURN 4
END
INSERT INTO Achievement VALUES (@gameId, @name, @description, @stages, @image)
SET @achievementId = @@IDENTITY
RETURN 0
GO
----------------------------
-- ADD IF NOT ACHIEVEMENT --
----------------------------
CREATE PROCEDURE AddIfNotAchievement(
@gameId INT,
@name VARCHAR(128),
@description VARCHAR(512),
@stages INT,
@image VARCHAR(11),
@achievementId INT OUTPUT
)
AS
IF NOT @gameId IN (SELECT ID FROM Game)
BEGIN
PRINT 'No game with the specified ID was found'
RETURN 1
END
IF @name IS NULL
BEGIN
PRINT 'Achievement name cannot be null'
RETURN 2
END
IF @stages IS NULL
BEGIN
PRINT 'Achievement stages cannot be null'
RETURN 3
END
IF NOT @name IN (SELECT [Name] FROM Achievement WHERE GameID = @gameId)
BEGIN
INSERT INTO Achievement VALUES (@gameId, @name, @description, @stages, @image)
END
SELECT @achievementId = ID FROM Achievement WHERE [Name] = @name AND GameID = @gameId
RETURN 0
GO
------------------------
-- REMOVE ACHIEVEMENT --
------------------------
CREATE PROCEDURE RemoveAchievement(
@achievementId INT
)
AS
IF NOT @achievementId IN (SELECT ID FROM Achievement)
BEGIN
PRINT 'No achievement with the specified ID was found'
RETURN 1
END
DELETE FROM Achievement WHERE ID = @achievementId
RETURN 0
GO
--------------------------
-- GET ACHIEVEMENT ICON --
--------------------------
CREATE PROCEDURE GetAchievementIcon(
@achievementId INT
)
AS
IF NOT @achievementId IN (SELECT ID FROM Achievement)
BEGIN
PRINT 'No achievement with the specified ID was found'
RETURN 1
END
SELECT Icon FROM Achievement WHERE ID = @achievementId
RETURN 0
GO
---------------------------------------
-- SET ACHIEVEMENT PROGRESS FOR USER --
---------------------------------------
CREATE PROCEDURE SetAchievementProgressForUser(
@userId INT,
@platformId INT,
@achievementId INT,
@progress INT
)
AS
IF NOT @userId IN (SELECT ID FROM [User])
BEGIN
PRINT 'No user with the specified ID was found'
RETURN 1
END
IF NOT @platformId IN (SELECT ID FROM [Platform])
BEGIN
PRINT 'No platform with the specified ID was found'
RETURN 2
END
IF NOT @achievementId IN (SELECT ID FROM Achievement)
BEGIN
PRINT 'No achievement with the specified ID was found'
RETURN 3
END
IF EXISTS (SELECT * FROM Progress WHERE AchievementID = @achievementId AND UserID = @userId AND PlatformID = @platformId)
BEGIN
UPDATE Progress SET Progress = @progress WHERE AchievementID = @achievementId AND UserID = @userId AND PlatformID = @platformId
END
ELSE
BEGIN
INSERT INTO Progress VALUES (@userId, @platformId, @achievementId, @progress)
END
RETURN 0
GO

60
sql/SearchProcs.sql Normal file
View file

@ -0,0 +1,60 @@
-----------------------
-- SEARCH ACHIEVEMENTS --
-----------------------
CREATE PROCEDURE SearchAchievements(
@searchTerm VARCHAR(32),
@userId INT,
@completed BIT,
@minCompletion FLOAT,
@maxCompletion FLOAT,
@minDifficulty FLOAT,
@maxDifficulty FLOAT,
@minQuality FLOAT,
@maxQuality FLOAT
)
AS
IF @userId IS NULL AND @completed = 1
BEGIN
PRINT 'Cannot search for completed achievements with no user specified'
RETURN 1
END
IF @searchTerm IS NULL OR @searchTerm = ''
SET @searchTerm = '%'
ELSE
SET @searchTerm = '%' + @searchTerm + '%'
PRINT @searchTerm
IF NOT @userId IS NULL
SELECT TOP 100 Game.[Name] AS Game, Achievement.[Name], Completion, Difficulty, Quality
FROM Achievement
JOIN MaxProgress ON AchievementID = Achievement.ID AND UserID = @userId
JOIN Game ON Game.ID = GameID
JOIN AchievementCompletion AC ON AC.ID = Achievement.ID
JOIN AchievementRatings AR ON AR.ID = Achievement.ID
WHERE (Game.[Name] LIKE @searchTerm OR Achievement.[Name] LIKE @searchTerm)
AND (@completed <> 1 OR Progress = Stages )
AND (@minCompletion IS NULL OR @minCompletion <= Completion)
AND (@maxCompletion IS NULL OR @maxCompletion >= Completion)
AND (@minDifficulty IS NULL OR @minDifficulty <= Difficulty)
AND (@maxDifficulty IS NULL OR @maxDifficulty >= Difficulty)
AND (@minQuality IS NULL OR @minQuality <= Quality )
AND (@maxQuality IS NULL OR @maxQuality >= Quality )
ELSE
SELECT TOP 100 Achievement.ID, Game.[Name] AS Game, Achievement.[Name], Completion, Quality, Difficulty
FROM Achievement
JOIN Game ON Game.ID = GameID
JOIN AchievementCompletion AC ON AC.ID = Achievement.ID
JOIN AchievementRatings AR ON AR.ID = Achievement.ID
WHERE (Game.[Name] LIKE @searchTerm OR Achievement.[Name] LIKE @searchTerm)
AND (@minCompletion IS NULL OR @minCompletion <= Completion)
AND (@maxCompletion IS NULL OR @maxCompletion >= Completion)
AND (@minDifficulty IS NULL OR @minDifficulty <= Difficulty)
AND (@maxDifficulty IS NULL OR @maxDifficulty >= Difficulty)
AND (@minQuality IS NULL OR @minQuality <= Quality )
AND (@maxQuality IS NULL OR @maxQuality >= Quality )
RETURN 0
GO
EXEC SearchAchievements '', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL

View file

@ -27,6 +27,9 @@
-----------------------------
--CREATE TYPE ImageType FROM VARCHAR(4) NULL
--GO
CREATE TABLE [User] (
ID INT IDENTITY(0, 1) NOT NULL,
Email VARCHAR(254) NOT NULL,
@ -36,7 +39,9 @@ CREATE TABLE [User] (
Hue INT NOT NULL
CONSTRAINT HueDefault DEFAULT 0
CONSTRAINT HueConstraint CHECK (0 <= Hue AND Hue <= 360),
PFP VARCHAR(11) NULL,
ProfileImage ImageType,
[Admin] BIT NOT NULL
CONSTRAINT AdmivDefault DEFAULT 0,
Verified BIT NOT NULL
CONSTRAINT VerifiedDefault DEFAULT 0
PRIMARY KEY(ID)
@ -44,14 +49,15 @@ CREATE TABLE [User] (
CREATE TABLE [Platform] (
ID INT IDENTITY(0, 1) NOT NULL,
PlatformName VARCHAR(32) NOT NULL
PlatformName VARCHAR(32) NOT NULL,
Icon ImageType
PRIMARY KEY(ID)
)
CREATE TABLE [Game] (
ID INT IDENTITY(0, 1) NOT NULL,
Name VARCHAR(32) NOT NULL,
Thumbnail VARCHAR(256) NULL
Icon ImageType
PRIMARY KEY(ID)
)
@ -61,7 +67,7 @@ CREATE TABLE [Achievement] (
Name VARCHAR(128) NOT NULL,
Description VARCHAR(512) NULL,
Stages INT NOT NULL,
Thumbnail VARCHAR(256) NULL
Icon ImageType
PRIMARY KEY(ID)
FOREIGN KEY(GameID) REFERENCES [Game](ID)
ON UPDATE CASCADE
@ -89,7 +95,7 @@ CREATE TABLE [Progress] (
PlatformID INT NOT NULL,
AchievementID INT NOT NULL,
Progress INT NOT NULL
PRIMARY KEY(UserID, AchievementID)
PRIMARY KEY(UserID, PlatformID, AchievementID)
FOREIGN KEY(UserID) REFERENCES [User](ID)
ON UPDATE CASCADE
ON DELETE CASCADE,
@ -117,7 +123,7 @@ CREATE TABLE [IsOn] (
CREATE TABLE [ExistsOn] (
GameID INT NOT NULL,
PlatformID INT NOT NULL,
PlatformGameID INT NOT NULL
PlatformGameID VARCHAR(32) NOT NULL
PRIMARY KEY(GameID, PlatformID)
FOREIGN KEY(GameID) REFERENCES [Game](ID)
ON UPDATE CASCADE

View file

@ -1,186 +0,0 @@
---------------------------------------
-- GET USER NAME AND STATS PROCEDURE --
---------------------------------------
CREATE PROCEDURE GetUserNameAndStats(
@userId INT,
@username VARCHAR(32) OUTPUT,
@completed INT OUTPUT,
@average INT OUTPUT,
@perfect INT OUTPUT
)
AS
BEGIN TRANSACTION
SELECT @username = Username
FROM [User]
WHERE ID = @userId
IF @username IS NULL
BEGIN
PRINT 'No user found with specified id'
ROLLBACK TRANSACTION
RETURN 1
END
DECLARE @progress TABLE (GameID INT, Completed INT, Total INT)
INSERT INTO @progress
SELECT GameID, SUM(CASE WHEN Progress.Progress = Achievement.Stages THEN 1 ELSE 0 END) AS Completed, COUNT(AchievementID) AS Total
FROM Achievement
JOIN Progress ON
Progress.UserID = @userId
AND Progress.AchievementID = Achievement.ID
GROUP BY GameID
COMMIT TRANSACTION
SELECT @completed = SUM(Completed)
FROM @progress
SELECT @average = AVG((Completed * 100) / Total)
FROM @progress
SELECT @perfect = COUNT(GameID)
FROM @progress
WHERE Completed = Total
RETURN 0
GO
----------------------------------
-- GET USER PLATFORMS PROCEDURE --
----------------------------------
CREATE PROCEDURE GetUserPlatforms(
@userId INT
)
AS
SELECT [Platform].ID, [PlatformName], (CASE WHEN UserID IS NOT NULL THEN 1 ELSE 0 END) AS Connected
FROM [Platform]
LEFT JOIN IsOn ON IsOn.PlatformID = [Platform].ID
ORDER BY [Platform].ID
GO
--------------------------------
-- GET USER RATINGS PROCEDURE --
--------------------------------
CREATE PROCEDURE GetUserRatings(
@userId INT
)
AS
SELECT Game.Name AS GameName, Achievement.Name AS AchievementName, Quality, Difficulty, Rating.[Description]
FROM Rating
JOIN Achievement ON Achievement.ID = Rating.AchievementID
JOIN Game ON Game.ID = Achievement.GameID
WHERE UserID = @userId
GO
------------------------------
-- GET USER IMAGE PROCEDURE --
------------------------------
CREATE PROCEDURE GetUserImage(
@userId INT
)
AS
IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId)
BEGIN
PRINT 'No user with specified ID found'
RETURN 1
END
SELECT PFP FROM [User] WHERE ID = @userId
RETURN 0
GO
------------------
-- SET USERNAME --
------------------
CREATE PROCEDURE SetUsername(
@userId INT,
@username VARCHAR(32)
)
AS
IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId)
BEGIN
PRINT 'No user with specified ID found'
RETURN 1
END
UPDATE [User] SET Username = @username WHERE ID = @userId
RETURN 0
GO
------------------------------
-- SET USER IMAGE PROCEDURE --
------------------------------
CREATE PROCEDURE SetUserImage(
@userId INT,
@type VARCHAR(11)
)
AS
IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId)
BEGIN
PRINT 'No user with specified ID found'
RETURN 1
END
UPDATE [User] SET PFP = @type WHERE ID = @userId
RETURN 0
GO
---------------------------
-- ADD USER TO PROCEDURE --
---------------------------
CREATE PROCEDURE AddPlatform(
@userId INT,
@platformId INT,
@platformUserID VARCHAR(32)
)
AS
IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId)
BEGIN
PRINT 'No user with specified ID found'
RETURN 1
END
IF NOT EXISTS (SELECT * FROM [Platform] WHERE ID = @platformId)
BEGIN
PRINT 'No platform with specified ID found'
RETURN 2
END
IF EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'User already exists on platform'
RETURN 3
END
INSERT INTO IsOn VALUES (@userId, @platformId, @platformUserId)
RETURN 0
GO
--------------------------------
-- REMOVE USER FROM PROCEDURE --
--------------------------------
CREATE PROCEDURE RemovePlatform(
@userId INT,
@platformId INT
)
AS
IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId)
BEGIN
PRINT 'No user with specified ID found'
RETURN 1
END
IF NOT EXISTS (SELECT * FROM [Platform] WHERE ID = @platformId)
BEGIN
PRINT 'No platform with specified ID found'
RETURN 2
END
IF NOT EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId)
BEGIN
PRINT 'User does not exist on platform'
RETURN 3
END
DELETE FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId
RETURN 0
GO

35
sql/Views.sql Normal file
View file

@ -0,0 +1,35 @@
-- The maximum progress a user has on an achievement across all platforms
CREATE VIEW MaxProgress
AS
SELECT UserID, AchievementID, MAX(Progress) AS Progress
FROM Progress
GROUP BY UserID, AchievementID
GO
-- List of games and users with the number of completed achievements out of the total achievements the user has completed
CREATE VIEW GameCompletionByUser
AS
SELECT UserID, GameID, SUM(CASE WHEN Progress = Stages THEN 1 ELSE 0 END) AS Completed, COUNT(AchievementID) AS Total
FROM Achievement
JOIN MaxProgress ON AchievementID = Achievement.ID
GROUP BY UserID, GameID
GO
-- List of achievements and the percentage of people who have completed it
CREATE VIEW AchievementCompletion
AS
SELECT Achievement.ID, (CASE WHEN COUNT(UserID) = 0 THEN NULL ELSE (SUM(CASE WHEN Progress = Stages THEN 1 ELSE 0 END) * 100 / COUNT(UserID)) END) AS Completion
FROM Achievement
LEFT JOIN MaxProgress ON AchievementID = Achievement.ID
GROUP BY Achievement.ID
GO
-- List of achievements and their average quality and difficulty ratings filling with null as necessary
CREATE VIEW AchievementRatings
AS
SELECT Achievement.ID, AVG(Quality) AS Quality, AVG(Difficulty) AS Difficulty
FROM Achievement
LEFT JOIN Rating ON AchievementID = Achievement.ID
GROUP BY Achievement.ID
GO