A heckin ton. Mostly hackish
6
backend/.gitignore
vendored
|
@ -22,3 +22,9 @@ src/main/resources/application-local.properties
|
||||||
|
|
||||||
# Server Keystore
|
# Server Keystore
|
||||||
src/main/resources/achievements-ssl-key.p12
|
src/main/resources/achievements-ssl-key.p12
|
||||||
|
|
||||||
|
# Api Keys
|
||||||
|
apikeys/
|
||||||
|
|
||||||
|
# Program Data
|
||||||
|
images/
|
|
@ -1,20 +1,23 @@
|
||||||
package achievements;
|
package achievements;
|
||||||
|
|
||||||
import achievements.misc.DbConnectionService;
|
import achievements.misc.DbConnection;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
|
||||||
|
@EnableScheduling
|
||||||
public class Application {
|
public class Application {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
var context = SpringApplication.run(Application.class, args);
|
var context = SpringApplication.run(Application.class, args);
|
||||||
|
|
||||||
// Verify the database connection succeeded
|
// Verify the database connection succeeded
|
||||||
var db = context.getBean(DbConnectionService.class);
|
var db = context.getBean(DbConnection.class);
|
||||||
if (db.getConnection() == null) {
|
if (db.getConnection() == null) {
|
||||||
SpringApplication.exit(context, () -> 0);
|
SpringApplication.exit(context, () -> 0);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +29,7 @@ public class Application {
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry
|
registry
|
||||||
.addMapping("/*")
|
.addMapping("/**")
|
||||||
.allowedOrigins("*");
|
.allowedOrigins("*");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package achievements.controllers;
|
|
||||||
|
|
||||||
import achievements.data.Profile;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
|
|
||||||
public class DataController {
|
|
||||||
|
|
||||||
public void getProfile(@RequestBody Profile.Query query) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,13 +7,13 @@ import achievements.services.AuthenticationService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
public class LoginController {
|
public class LoginController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -26,17 +26,11 @@ public class LoginController {
|
||||||
*
|
*
|
||||||
* -1 => Unknown error
|
* -1 => Unknown error
|
||||||
*/
|
*/
|
||||||
@RequestMapping(value = "/create_user", method = POST, consumes = "application/json", produces = "application/json")
|
@PostMapping(value = "/create_user", consumes = "application/json", produces = "application/json")
|
||||||
public ResponseEntity createUser(@RequestBody User user) {
|
public ResponseEntity createUser(@RequestBody User user) {
|
||||||
var response = authService.createUser(user);
|
var response = authService.createUser(user);
|
||||||
if (response.status == 0) {
|
if (response.status == 0) {
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.status(HttpStatus.CREATED).body(response.session);
|
||||||
new Session(
|
|
||||||
authService.session().generate(response.id),
|
|
||||||
response.id,
|
|
||||||
response.hue
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (response.status > 0) {
|
} else if (response.status > 0) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(response.status));
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(response.status));
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,17 +50,11 @@ public class LoginController {
|
||||||
*
|
*
|
||||||
* -1 => Unknown error
|
* -1 => Unknown error
|
||||||
*/
|
*/
|
||||||
@RequestMapping(value = "/login", method = POST, consumes = "application/json", produces = "application/json")
|
@PostMapping(value = "/login", consumes = "application/json", produces = "application/json")
|
||||||
public ResponseEntity login(@RequestBody User user) {
|
public ResponseEntity login(@RequestBody User user) {
|
||||||
var response = authService.login(user);
|
var response = authService.login(user);
|
||||||
if (response.status == 0) {
|
if (response.status == 0) {
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(response.session);
|
||||||
new Session(
|
|
||||||
authService.session().generate(response.id),
|
|
||||||
response.id,
|
|
||||||
response.hue
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (response.status > 0) {
|
} else if (response.status > 0) {
|
||||||
// Hardcoded 1 response code
|
// Hardcoded 1 response code
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(1));
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(1));
|
||||||
|
@ -75,7 +63,16 @@ public class LoginController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/logout", method = POST, consumes = "application/json", produces = "application/json")
|
@PostMapping(value = "/refresh", consumes = "application/json", produces = "application/json")
|
||||||
|
public ResponseEntity refresh(@RequestBody Session key) {
|
||||||
|
if (authService.refresh(key)) {
|
||||||
|
return ResponseEntity.ok("{}");
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/logout", consumes = "application/json")
|
||||||
public ResponseEntity logout(@RequestBody Session session) {
|
public ResponseEntity logout(@RequestBody Session session) {
|
||||||
authService.logout(session);
|
authService.logout(session);
|
||||||
return ResponseEntity.ok("{}");
|
return ResponseEntity.ok("{}");
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package achievements.controllers;
|
||||||
|
|
||||||
|
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.RestController;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class PlatformController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PlatformService platforms;
|
||||||
|
|
||||||
|
@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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
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.SetUsername;
|
||||||
|
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")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/{user}", produces = "application/json")
|
||||||
|
public ResponseEntity getProfile(@PathVariable("user") int user) {
|
||||||
|
var profile = userService.getProfile(user);
|
||||||
|
if (profile == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new APError(1, "Failed to get user profile"));
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.ok(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/{user}/username", consumes = "application/json", produces = "application/json")
|
||||||
|
public ResponseEntity setUsername(@PathVariable("user") int userId, @RequestBody SetUsername username) {
|
||||||
|
var name = userService.setUsername(userId, username);
|
||||||
|
if (name == 0) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body("{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/{user}/image")
|
||||||
|
public void getProfilePicture(@PathVariable("user") int user, HttpServletResponse response) {
|
||||||
|
var 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
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();
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body("{ \"code\": 0, \"message\": \"Success\" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": -1, \"message\": \"Unknown error\" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/{user}/platforms/add", consumes = "application/json", produces = "application/json")
|
||||||
|
public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatformRequest request) {
|
||||||
|
var result = userService.addPlatform(userId, request);
|
||||||
|
if (result == 0) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.badRequest().body("{}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/{user}/platforms/remove", consumes = "application/json", produces = "application/json")
|
||||||
|
public ResponseEntity removePlatformForUser(@PathVariable("user") int userId, @RequestBody RemovePlatformRequest request) {
|
||||||
|
var result = userService.removePlatform(userId, request);
|
||||||
|
if (result == 0) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body("{}");
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.badRequest().body("{}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
backend/src/main/java/achievements/data/APPostRequest.java
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package achievements.data;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class APPostRequest {
|
||||||
|
|
||||||
|
@JsonProperty("key")
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,29 +7,13 @@ import java.util.List;
|
||||||
|
|
||||||
public class Profile {
|
public class Profile {
|
||||||
|
|
||||||
public static class Query {
|
public static class Platform {
|
||||||
@JsonProperty("username")
|
@JsonProperty
|
||||||
private StringFilter string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonProperty("id")
|
|
||||||
private int id;
|
private int id;
|
||||||
@JsonProperty("username")
|
@JsonProperty("name")
|
||||||
private String username;
|
private String name;
|
||||||
@JsonProperty("plaforms")
|
@JsonProperty("connected")
|
||||||
private List<String> platforms;
|
private boolean connected;
|
||||||
@JsonProperty("games")
|
|
||||||
private List<Game> games;
|
|
||||||
@JsonProperty("achievements")
|
|
||||||
private List<Achievement> achievements;
|
|
||||||
|
|
||||||
public Profile(int id, String username, List<String> platforms, List<Game> games, List<Achievement> achievements) {
|
|
||||||
this.id = id;
|
|
||||||
this.username = username;
|
|
||||||
this.platforms = platforms;
|
|
||||||
this.games = games;
|
|
||||||
this.achievements = achievements;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return id;
|
return id;
|
||||||
|
@ -39,6 +23,38 @@ public class Profile {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getConnected() {
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnected(boolean connected) {
|
||||||
|
this.connected = connected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonProperty("username")
|
||||||
|
private String username;
|
||||||
|
@JsonProperty("completed")
|
||||||
|
private int completed;
|
||||||
|
@JsonProperty("average")
|
||||||
|
private Integer average;
|
||||||
|
@JsonProperty("perfect")
|
||||||
|
private int perfect;
|
||||||
|
@JsonProperty("noteworthy")
|
||||||
|
private List<Achievement> noteworthy;
|
||||||
|
@JsonProperty("platforms")
|
||||||
|
private List<Platform> platforms;
|
||||||
|
/*@JsonProperty("ratings")
|
||||||
|
private List<Rating> ratings;*/
|
||||||
|
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
@ -47,27 +63,43 @@ public class Profile {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getPlatforms() {
|
public int getCompleted() {
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCompleted(int completed) {
|
||||||
|
this.completed = completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getAverage() {
|
||||||
|
return average;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAverage(Integer average) {
|
||||||
|
this.average = average;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPerfect() {
|
||||||
|
return perfect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPerfect(int perfect) {
|
||||||
|
this.perfect = perfect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Achievement> getNoteworthy() {
|
||||||
|
return noteworthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNoteworthy(List<Achievement> noteworthy) {
|
||||||
|
this.noteworthy = noteworthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Platform> getPlatforms() {
|
||||||
return platforms;
|
return platforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPlatforms(List<String> platforms) {
|
public void setPlatforms(List<Platform> platforms) {
|
||||||
this.platforms = platforms;
|
this.platforms = platforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Game> getGames() {
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGames(List<Game> games) {
|
|
||||||
this.games = games;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Achievement> getAchievements() {
|
|
||||||
return achievements;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAchievements(List<Achievement> achievements) {
|
|
||||||
this.achievements = achievements;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package achievements.data;
|
package achievements.data;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
public class Session {
|
public class Session {
|
||||||
|
@ -10,11 +11,14 @@ public class Session {
|
||||||
private int id;
|
private int id;
|
||||||
@JsonProperty("hue")
|
@JsonProperty("hue")
|
||||||
private int hue;
|
private int hue;
|
||||||
|
@JsonIgnore
|
||||||
|
private boolean used;
|
||||||
|
|
||||||
public Session(String key, int id, int hue) {
|
public Session(String key, int id, int hue) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.hue = hue;
|
this.hue = hue;
|
||||||
|
this.used = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
|
@ -40,4 +44,12 @@ public class Session {
|
||||||
public void setHue(int hue) {
|
public void setHue(int hue) {
|
||||||
this.hue = hue;
|
this.hue = hue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getUsed() {
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsed(boolean used) {
|
||||||
|
this.used = used;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package achievements.data.query;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class AddPlatformRequest {
|
||||||
|
@JsonProperty("sessionKey")
|
||||||
|
private String sessionKey;
|
||||||
|
@JsonProperty("platformId")
|
||||||
|
private int platformId;
|
||||||
|
@JsonProperty("platformUserId")
|
||||||
|
private String platformUserId;
|
||||||
|
|
||||||
|
public String getSessionKey() {
|
||||||
|
return sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionKey(String sessionKey) {
|
||||||
|
this.sessionKey = sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlatformId() {
|
||||||
|
return platformId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatformId(int platformId) {
|
||||||
|
this.platformId = platformId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlatformUserId() {
|
||||||
|
return platformUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatformUserId(String platformUserId) {
|
||||||
|
this.platformUserId = platformUserId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package achievements.data.query;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class RemovePlatformRequest {
|
||||||
|
@JsonProperty("sessionKey")
|
||||||
|
private String sessionKey;
|
||||||
|
@JsonProperty("platformId")
|
||||||
|
private int platformId;
|
||||||
|
|
||||||
|
public String getSessionKey() {
|
||||||
|
return sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionKey(String sessionKey) {
|
||||||
|
this.sessionKey = sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlatformId() {
|
||||||
|
return platformId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatformId(int platformId) {
|
||||||
|
this.platformId = platformId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package achievements.data.query;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class SetUsername {
|
||||||
|
@JsonProperty("sessionKey")
|
||||||
|
private String sessionKey;
|
||||||
|
@JsonProperty("userId")
|
||||||
|
private int userId;
|
||||||
|
@JsonProperty("username")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
public String getSessionKey() {
|
||||||
|
return sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionKey(String sessionKey) {
|
||||||
|
this.sessionKey = sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(int userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import java.sql.SQLException;
|
||||||
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
|
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DbConnectionService {
|
public class DbConnection {
|
||||||
|
|
||||||
private Connection connection;
|
private Connection connection;
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ public class DbConnectionService {
|
||||||
@Value("${database.user.password}")
|
@Value("${database.user.password}")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
public DbConnectionService() {}
|
public DbConnection() {}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void connect() {
|
public void connect() {
|
|
@ -1,26 +1,64 @@
|
||||||
package achievements.misc;
|
package achievements.misc;
|
||||||
|
|
||||||
|
import achievements.data.Session;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
@Component
|
||||||
public class SessionManager {
|
public class SessionManager {
|
||||||
|
|
||||||
private HashMap<String, Integer> session;
|
private HashMap<String, Session> sessions;
|
||||||
|
|
||||||
public SessionManager() {
|
public SessionManager() {
|
||||||
session = new HashMap();
|
sessions = new HashMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generate(Integer user) {
|
public Session generate(int user, int hue) {
|
||||||
var key = HashManager.encode(HashManager.generateBytes(16));
|
var key = HashManager.encode(HashManager.generateBytes(16));
|
||||||
session.put(key, user);
|
var session = new Session(key, user, hue);
|
||||||
return key;
|
sessions.put(key, session);
|
||||||
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getUser(String key) {
|
public int getUser(String key) {
|
||||||
return session.get(key);
|
return sessions.get(key).getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void remove(String key) {
|
public void remove(String key) {
|
||||||
session.remove(key);
|
sessions.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validate(int user, String key) {
|
||||||
|
var foreign = sessions.get(key);
|
||||||
|
return foreign != null && user == foreign.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean refresh(String key) {
|
||||||
|
var foreign = sessions.get(key);
|
||||||
|
if (foreign != null) {
|
||||||
|
foreign.setUsed(true);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up inactive sessions
|
||||||
|
@Scheduled(cron = "0 */30 * * * *")
|
||||||
|
public void clean() {
|
||||||
|
var remove = new ArrayList<String>();
|
||||||
|
sessions.forEach((key, session) -> {
|
||||||
|
if (!session.getUsed()) {
|
||||||
|
remove.add(session.getKey());
|
||||||
|
} else {
|
||||||
|
session.setUsed(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (var session : remove) {
|
||||||
|
sessions.remove(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package achievements.services;
|
||||||
|
|
||||||
import achievements.data.Session;
|
import achievements.data.Session;
|
||||||
import achievements.data.User;
|
import achievements.data.User;
|
||||||
import achievements.misc.DbConnectionService;
|
import achievements.misc.DbConnection;
|
||||||
import achievements.misc.Password;
|
import achievements.misc.Password;
|
||||||
import achievements.misc.SessionManager;
|
import achievements.misc.SessionManager;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -16,37 +16,33 @@ public class AuthenticationService {
|
||||||
|
|
||||||
public static class LoginResponse {
|
public static class LoginResponse {
|
||||||
public int status;
|
public int status;
|
||||||
public Integer id;
|
public Session session;
|
||||||
public int hue;
|
|
||||||
|
|
||||||
public LoginResponse() {
|
public LoginResponse() {
|
||||||
this.status = 0;
|
this.status = 0;
|
||||||
this.id = null;
|
|
||||||
this.hue = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginResponse(int status) {
|
public LoginResponse(int status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.id = null;
|
this.session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginResponse(int status, int id, int hue) {
|
public LoginResponse(int status, Session session) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.id = id;
|
this.session = session;
|
||||||
this.hue = hue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DbConnectionService dbs;
|
private DbConnection dbs;
|
||||||
private Connection db;
|
private Connection db;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
private SessionManager session;
|
private SessionManager session;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
private void init() {
|
private void init() {
|
||||||
db = dbs.getConnection();
|
db = dbs.getConnection();
|
||||||
session = new SessionManager();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginResponse createUser(User user) {
|
public LoginResponse createUser(User user) {
|
||||||
|
@ -70,8 +66,10 @@ public class AuthenticationService {
|
||||||
statement.execute();
|
statement.execute();
|
||||||
var response = new LoginResponse(
|
var response = new LoginResponse(
|
||||||
statement.getInt(1),
|
statement.getInt(1),
|
||||||
|
session.generate(
|
||||||
statement.getInt(6),
|
statement.getInt(6),
|
||||||
statement.getInt(7)
|
statement.getInt(7)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
statement.close();
|
statement.close();
|
||||||
|
|
||||||
|
@ -93,7 +91,13 @@ public class AuthenticationService {
|
||||||
var salt = result.getString("Salt");
|
var salt = result.getString("Salt");
|
||||||
var hash = result.getString("Password");
|
var hash = result.getString("Password");
|
||||||
if (Password.validate(salt, user.getPassword(), hash)) {
|
if (Password.validate(salt, user.getPassword(), hash)) {
|
||||||
response = new LoginResponse(0, result.getInt("ID"), result.getInt("Hue"));
|
response = new LoginResponse(
|
||||||
|
0,
|
||||||
|
session.generate(
|
||||||
|
result.getInt("ID"),
|
||||||
|
result.getInt("Hue")
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
response = new LoginResponse(2);
|
response = new LoginResponse(2);
|
||||||
}
|
}
|
||||||
|
@ -107,6 +111,8 @@ public class AuthenticationService {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean refresh(Session key) { return session.refresh(key.getKey()); }
|
||||||
|
|
||||||
public void logout(Session key) {
|
public void logout(Session key) {
|
||||||
session.remove(key.getKey());
|
session.remove(key.getKey());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package achievements.services;
|
|
||||||
|
|
||||||
import achievements.data.Achievement;
|
|
||||||
import achievements.data.Game;
|
|
||||||
import achievements.data.Profile;
|
|
||||||
import achievements.misc.DbConnectionService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class DataService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private DbConnectionService dbs;
|
|
||||||
private Connection db;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AuthenticationService auth;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
private void init() {
|
|
||||||
db = dbs.getConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*public List<Achievement> getUsers() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Game> getGames() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Profile> getProfiles() {
|
|
||||||
|
|
||||||
}*/
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package achievements.services;
|
||||||
|
|
||||||
|
import achievements.misc.DbConnection;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.sql.Connection;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PlatformService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DbConnection dbs;
|
||||||
|
private Connection db;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void init() {
|
||||||
|
db = dbs.getConnection();
|
||||||
|
}
|
||||||
|
}
|
193
backend/src/main/java/achievements/services/UserService.java
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package achievements.services;
|
||||||
|
|
||||||
|
import achievements.data.Profile;
|
||||||
|
import achievements.data.query.AddPlatformRequest;
|
||||||
|
import achievements.data.query.RemovePlatformRequest;
|
||||||
|
import achievements.data.query.SetUsername;
|
||||||
|
import achievements.misc.DbConnection;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DbConnection dbs;
|
||||||
|
private Connection db;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationService auth;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void init() {
|
||||||
|
db = dbs.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profile getProfile(int userId) {
|
||||||
|
try {
|
||||||
|
var profile = (Profile) null;
|
||||||
|
{
|
||||||
|
var stmt = db.prepareCall("{? = call GetUserNameAndStats(?, ?, ?, ?, ?)}");
|
||||||
|
stmt.registerOutParameter(1, Types.INTEGER);
|
||||||
|
stmt.setInt(2, userId);
|
||||||
|
stmt.registerOutParameter(3, Types.VARCHAR);
|
||||||
|
stmt.registerOutParameter(4, Types.INTEGER);
|
||||||
|
stmt.registerOutParameter(5, Types.INTEGER);
|
||||||
|
stmt.registerOutParameter(6, Types.INTEGER);
|
||||||
|
|
||||||
|
stmt.execute();
|
||||||
|
if (stmt.getInt(1) == 0) {
|
||||||
|
profile = new Profile();
|
||||||
|
profile.setUsername(stmt.getString(3));
|
||||||
|
profile.setCompleted(stmt.getInt(4));
|
||||||
|
var average = stmt.getString(5);
|
||||||
|
profile.setPerfect(stmt.getInt(6));
|
||||||
|
|
||||||
|
if (average != null) {
|
||||||
|
profile.setAverage(Integer.parseInt(average));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var stmt = db.prepareCall("{call GetUserPlatforms(?)}");
|
||||||
|
stmt.setInt(1, userId);
|
||||||
|
|
||||||
|
var results = stmt.executeQuery();
|
||||||
|
var platforms = new ArrayList<Profile.Platform>();
|
||||||
|
while (results.next()) {
|
||||||
|
var platform = new Profile.Platform();
|
||||||
|
platform.setId (results.getInt ("ID" ));
|
||||||
|
platform.setName (results.getString ("PlatformName"));
|
||||||
|
platform.setConnected(results.getBoolean("Connected" ));
|
||||||
|
platforms.add(platform);
|
||||||
|
}
|
||||||
|
profile.setPlatforms(platforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int setUsername(int userId, SetUsername username) {
|
||||||
|
try {
|
||||||
|
if (auth.session().validate(userId, username.getSessionKey()) && username.getUsername().length() > 0 && username.getUsername().length() <= 32) {
|
||||||
|
var stmt = db.prepareCall("{call SetUsername(?, ?)}");
|
||||||
|
stmt.setInt(1, userId);
|
||||||
|
stmt.setString(2, username.getUsername());
|
||||||
|
|
||||||
|
stmt.execute();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String setProfileImageType(int userId, String sessionKey, String type) {
|
||||||
|
try {
|
||||||
|
if (type.matches("image/.*")) {
|
||||||
|
type = type.substring(6);
|
||||||
|
var extension = VALID_IMAGE_TYPES.get(type);
|
||||||
|
if (!auth.session().validate(userId, sessionKey)) {
|
||||||
|
return "forbidden";
|
||||||
|
} else if (extension == null) {
|
||||||
|
return "unsupported_type";
|
||||||
|
} else {
|
||||||
|
var stmt = db.prepareCall("{call SetUserImage(?, ?)}");
|
||||||
|
stmt.setInt(1, userId);
|
||||||
|
stmt.setString(2, type);
|
||||||
|
|
||||||
|
stmt.execute();
|
||||||
|
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "not_an_image";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int addPlatform(int userId, AddPlatformRequest request) {
|
||||||
|
try {
|
||||||
|
if (auth.session().validate(userId, request.getSessionKey())) {
|
||||||
|
var stmt = db.prepareCall("{call AddPlatform(?, ?, ?)}");
|
||||||
|
stmt.setInt(1, userId);
|
||||||
|
stmt.setInt(2, request.getPlatformId());
|
||||||
|
stmt.setString(3, request.getPlatformUserId());
|
||||||
|
|
||||||
|
stmt.execute();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removePlatform(int userId, RemovePlatformRequest request) {
|
||||||
|
try {
|
||||||
|
if (auth.session().validate(userId, request.getSessionKey())) {
|
||||||
|
var stmt = db.prepareCall("{call RemovePlatform(?, ?)}");
|
||||||
|
stmt.setInt(1, userId);
|
||||||
|
stmt.setInt(2, request.getPlatformId());
|
||||||
|
|
||||||
|
stmt.execute();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,6 @@
|
||||||
server.port = 4730
|
server.port = 4730
|
||||||
spring.application.name = Achievements Project
|
spring.application.name = Achievements Project
|
||||||
|
spring.jackson.default-property-inclusion=always
|
||||||
|
|
||||||
|
server.session.cookie.secure = false
|
||||||
|
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
{}
|
{
|
||||||
|
"hosts": {
|
||||||
|
"backend": "https://localhost:4730"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
"extends": [
|
"extends": [
|
||||||
"config/base.json"
|
"config/base.json"
|
||||||
],
|
],
|
||||||
|
"hosts": {
|
||||||
|
"frontend": "http://localhost:8080"
|
||||||
|
},
|
||||||
"build": "debug",
|
"build": "debug",
|
||||||
"port": 8080
|
"port": 8080
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
"extends": [
|
"extends": [
|
||||||
"config/base.json"
|
"config/base.json"
|
||||||
],
|
],
|
||||||
|
"hosts": {
|
||||||
|
"frontend": "http://localhost"
|
||||||
|
},
|
||||||
"build": "release",
|
"build": "release",
|
||||||
"port": 80
|
"port": 80
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,19 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Cross platform achievement tracker",
|
"description": "Cross platform achievement tracker",
|
||||||
"repository": "github:Gnarwhal/AchievementProject",
|
"repository": "github:Gnarwhal/AchievementProject",
|
||||||
"main": "static_server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"debug": "node static_server.js config/debug.json",
|
"debug": "node server.js config/debug.json",
|
||||||
"release": "node static_server.js config/release.json"
|
"release": "node server.js config/release.json"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
"passport": "^0.4.1",
|
||||||
|
"passport-steam": "^1.0.15",
|
||||||
"promptly": "^3.2.0"
|
"promptly": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
62
frontend/server.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
const fs = require('fs' );
|
||||||
|
const path = require('path' );
|
||||||
|
|
||||||
|
const https = require('https' );
|
||||||
|
const express = require('express' );
|
||||||
|
const morgan = require('morgan' );
|
||||||
|
const passport = require('passport');
|
||||||
|
const SteamStrategy = require('passport-steam').Strategy;
|
||||||
|
|
||||||
|
const promptly = require('promptly');
|
||||||
|
|
||||||
|
const config = require('./config.js').load(process.argv[2]);
|
||||||
|
|
||||||
|
if (config.build === 'debug') {
|
||||||
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Running server at '${config.hosts.frontend}'`);
|
||||||
|
|
||||||
|
passport.use(new SteamStrategy({
|
||||||
|
returnURL: `${config.hosts.frontend}/profile/steam`,
|
||||||
|
realm: `${config.hosts.frontend}`,
|
||||||
|
profile: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use("/", morgan("dev"));
|
||||||
|
app.use("/static", express.static("webpage/static"));
|
||||||
|
app.get("/login", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname + "/webpage/login.html"));
|
||||||
|
});
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname + "/webpage/index.html"));
|
||||||
|
});
|
||||||
|
app.get("/about", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname + "/webpage/about.html"));
|
||||||
|
});
|
||||||
|
app.get("/profile/:id", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname + "/webpage/profile.html"));
|
||||||
|
});
|
||||||
|
app.get("/auth/steam", passport.authenticate('steam'), (req, res) => {});
|
||||||
|
|
||||||
|
// --- API Forward --- //
|
||||||
|
|
||||||
|
app.use("/api/*", (req, res) => {
|
||||||
|
res.redirect(307, `${config.hosts.backend}/${req.params[0]}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------- //
|
||||||
|
|
||||||
|
const server = app.listen(config.port);
|
||||||
|
|
||||||
|
const prompt = input => {
|
||||||
|
if (/q(?:uit)?|exit/i.test(input)) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
promptly.prompt('')
|
||||||
|
.then(prompt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
prompt();
|
|
@ -1,26 +0,0 @@
|
||||||
const express = require('express' );
|
|
||||||
const morgan = require('morgan' );
|
|
||||||
const promptly = require('promptly');
|
|
||||||
|
|
||||||
const config = require('./config.js').load(process.argv[2]);
|
|
||||||
|
|
||||||
if (config.build === 'debug') {
|
|
||||||
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use("/", morgan("dev"));
|
|
||||||
app.use("/", express.static("webpage"));
|
|
||||||
|
|
||||||
const server = app.listen(config.port);
|
|
||||||
|
|
||||||
const prompt = input => {
|
|
||||||
if (/q(?:uit)?|exit/i.test(input)) {
|
|
||||||
server.close();
|
|
||||||
} else {
|
|
||||||
promptly.prompt('')
|
|
||||||
.then(prompt);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
prompt();
|
|
42
frontend/webpage/about.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!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>
|
|
@ -4,9 +4,9 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Achievements Project</title>
|
<title>Achievements Project</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles/theme.css" />
|
<link rel="stylesheet" href="/static/styles/theme.css" />
|
||||||
<link rel="stylesheet" href="styles/common.css" />
|
<link rel="stylesheet" href="/static/styles/common.css" />
|
||||||
<link rel="stylesheet" href="styles/index.css" />
|
<link rel="stylesheet" href="/static/styles/index.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="navbar">
|
<div id="navbar">
|
||||||
|
@ -21,19 +21,111 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div id="content-body">
|
<div id="content-body">
|
||||||
<template data-template="content-body: List<Basic>">
|
<div id="index-page" class="page">
|
||||||
<div id="${page}-page" class="page">
|
|
||||||
<div class="page-subsection">
|
<div class="page-subsection">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<p class="page-header-text">${title}</p>
|
<p class="page-header-text">Achievements Project</p>
|
||||||
<div class="page-header-separator"></div>
|
<div class="page-header-separator"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template data-template="extern-${page}-page: Extern"></template>
|
<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">Achievements</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">My Games</p>
|
||||||
|
</div>
|
||||||
|
<div id="in-progress-filter" class="list-page-filter">
|
||||||
|
<div class="list-page-filter-checkbox"></div>
|
||||||
|
<p class="list-page-filter-name">In Progress</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<script src="scripts/template.js"></script>
|
</div>
|
||||||
<script src="scripts/index.js"></script>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/static/scripts/template.js"></script>
|
||||||
|
<script src="/static/scripts/common.js"></script>
|
||||||
|
<script src="/static/scripts/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -3,9 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Achievements Project | Login</title>
|
<title>Achievements Project | Login</title>
|
||||||
<link rel="stylesheet" href="styles/theme.css" />
|
<link rel="stylesheet" href="/static/styles/theme.css" />
|
||||||
<link rel="stylesheet" href="styles/common.css" />
|
<link rel="stylesheet" href="/static/styles/common.css" />
|
||||||
<link rel="stylesheet" href="styles/login.css" />
|
<link rel="stylesheet" href="/static/styles/login.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="navbar"></div>
|
<div id="navbar"></div>
|
||||||
|
@ -44,7 +44,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="scripts/template.js"></script>
|
<script src="/static/scripts/template.js"></script>
|
||||||
<script src="scripts/login.js"></script>
|
<script src="/static/scripts/common.js"></script>
|
||||||
|
<script src="/static/scripts/login.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
138
frontend/webpage/profile.html
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<!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/profile.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="profile-page" class="page">
|
||||||
|
<div class="page-subsection">
|
||||||
|
<div class="page-header">
|
||||||
|
<p class="page-header-text">Profile</p>
|
||||||
|
<div class="page-header-separator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template data-template="profile-page">
|
||||||
|
<div id="profile-section-1">
|
||||||
|
<div id="profile-info" class="page-subsection">
|
||||||
|
<div class="page-subsection-wrapper">
|
||||||
|
<div class="page-subheader">
|
||||||
|
<div id="profile-info-name" class="page-subheader-flex">
|
||||||
|
<p id="profile-info-username-text" class="page-subheader-text">${username}</p>
|
||||||
|
<input id="profile-info-username-field" class="page-subheader-text" value="${username}"/>
|
||||||
|
<span id="info-edit-stack" class="profile-edit-stack">
|
||||||
|
<img class="profile-edit page-subheader-icon" src="/static/res/edit.svg" alt="Edit Platforms" />
|
||||||
|
<img class="profile-edit-hover page-subheader-icon" src="/static/res/edit-hover.svg" alt="Edit Platforms" />
|
||||||
|
</span>
|
||||||
|
<span id="info-save-stack" class="profile-save-stack">
|
||||||
|
<img class="profile-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
|
||||||
|
<img class="profile-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Edit Platforms" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="page-subheader-separator"></div>
|
||||||
|
</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" />
|
||||||
|
<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" />
|
||||||
|
<img id="profile-info-pfp-upload-invalid" src="/static/res/upload-invalid.svg" alt="Invalid Image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-stats" class="page-subsection">
|
||||||
|
<div id="profile-stats-numeric">
|
||||||
|
<div id="profile-completion" class="page-subsection-wrapper">
|
||||||
|
<div class="page-subheader">
|
||||||
|
<p class="page-subheader-text">Avg. Completion</p>
|
||||||
|
<div class="page-subheader-separator"></div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-completion-stack">
|
||||||
|
<img id="profile-completion-background" src="/static/res/completion.svg">
|
||||||
|
<canvas id="profile-completion-canvas"></canvas>
|
||||||
|
<p id="profile-completion-text">${average}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-perfect" class="page-subsection-wrapper">
|
||||||
|
<div class="page-subheader">
|
||||||
|
<p class="page-subheader-text">Perfect Games</p>
|
||||||
|
<div class="page-subheader-separator"></div>
|
||||||
|
</div>
|
||||||
|
<p id="profile-perfect-text">${perfect}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-hardest" class="page-subsection-wrapper">
|
||||||
|
<div class="page-subheader">
|
||||||
|
<p class="page-subheader-text">Noteworthy Achievements</p>
|
||||||
|
<div class="page-subheader-separator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-section-2">
|
||||||
|
<div id="profile-platforms" class="page-subsection">
|
||||||
|
<div class="page-subsection-wrapper">
|
||||||
|
<div class="page-subheader">
|
||||||
|
<div class="page-subheader-flex">
|
||||||
|
<p class="page-subheader-text">Platforms</p>
|
||||||
|
<span id="platform-edit-stack" class="profile-edit-stack">
|
||||||
|
<img id="platform-edit" class="profile-edit page-subheader-icon" src="/static/res/edit.svg" alt="Edit Platforms" />
|
||||||
|
<img id="platform-edit-hover" class="profile-edit-hover page-subheader-icon" src="/static/res/edit-hover.svg" alt="Edit Platforms" />
|
||||||
|
</span>
|
||||||
|
<span id="platform-save-stack" class="profile-save-stack">
|
||||||
|
<img id="platform-save" class="profile-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
|
||||||
|
<img id="platform-save-hover" class="profile-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Edit Platforms" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="page-subheader-separator"></div>
|
||||||
|
</div>
|
||||||
|
<div class="profile-list page-subsection-chunk">
|
||||||
|
<template data-template="profile-platforms-list: List<Basic>">
|
||||||
|
<div class="profile-entry ${connected}">
|
||||||
|
${img}
|
||||||
|
<p class="profile-entry-text platform-name">${name}</p>
|
||||||
|
<div id="platform-${platform_id}" class="platform-remove-stack">
|
||||||
|
<img class="platform-remove" src="/static/res/cancel.svg" alt="Cancel" />
|
||||||
|
<img class="platform-remove-hover" src="/static/res/cancel-hover.svg" alt="Cancel" />
|
||||||
|
</div>
|
||||||
|
${add}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-ratings" class="page-subsection">
|
||||||
|
<div class="page-subsection-wrapper">
|
||||||
|
<div class="page-subheader">
|
||||||
|
<p class="page-subheader-text">Ratings</p>
|
||||||
|
<div class="page-subheader-separator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/static/scripts/template.js"></script>
|
||||||
|
<script src="/static/scripts/common.js"></script>
|
||||||
|
<script src="/static/scripts/profile.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 135 KiB |
|
@ -1,130 +0,0 @@
|
||||||
let session = null;
|
|
||||||
const loadSession = () => {
|
|
||||||
session = JSON.parse(window.sessionStorage.getItem('session'));
|
|
||||||
if (session) {
|
|
||||||
document.querySelector(":root").style.setProperty('--accent-hue', session.hue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandTemplates = async () => {
|
|
||||||
template.apply("navbar").values([
|
|
||||||
{ section: "left" },
|
|
||||||
{ section: "right" }
|
|
||||||
]);
|
|
||||||
template.apply("navbar-section-left").values([
|
|
||||||
{ item: "games", title: "Games" },
|
|
||||||
{ item: "achievements", title: "Achievements" }
|
|
||||||
]);
|
|
||||||
if (session) {
|
|
||||||
template.apply("navbar-section-right").values([
|
|
||||||
{ item: "profile", title: "Profile" },
|
|
||||||
{ item: "logout", title: "Logout" }
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
template.apply("navbar-section-right").values([
|
|
||||||
{ item: "login", title: "Login" }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
template.apply("content-body").values([
|
|
||||||
{ page: "games", title: "Games" },
|
|
||||||
{ page: "achievements", title: "Achievements" },
|
|
||||||
{ page: "profile", title: "Profile" }
|
|
||||||
]);
|
|
||||||
template.apply("extern-games-page" ).values("games_page" );
|
|
||||||
template.apply("extern-achievements-page").values("achievements_page");
|
|
||||||
template.apply("extern-profile-page" ).values("profile_page" );
|
|
||||||
|
|
||||||
await template.expand();
|
|
||||||
};
|
|
||||||
|
|
||||||
let pages = null;
|
|
||||||
const loadPages = () => {
|
|
||||||
pages = document.querySelectorAll(".page");
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectNavbar = () => {
|
|
||||||
const navItems = document.querySelectorAll(".navbar-item");
|
|
||||||
|
|
||||||
for (const item of navItems) {
|
|
||||||
if (item.dataset.pageName === "logout") {
|
|
||||||
item.addEventListener("click", (clickEvent) => {
|
|
||||||
fetch('https://localhost:4730/logout', {
|
|
||||||
method: 'POST',
|
|
||||||
mode: 'cors',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ key: session.key })
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
window.sessionStorage.removeItem('session');
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (item.dataset.pageName === "login") {
|
|
||||||
item.addEventListener("click", (clickEvent) => window.location.href = "/login.html");
|
|
||||||
} else {
|
|
||||||
item.addEventListener("click", (clickEvent) => {
|
|
||||||
const navItemPageId = item.dataset.pageName + "-page"
|
|
||||||
for (const page of pages) {
|
|
||||||
if (page.id === navItemPageId) {
|
|
||||||
page.style.display = "block";
|
|
||||||
} else {
|
|
||||||
page.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectProfile = () => {
|
|
||||||
const games = document.querySelector("#profile-games-header");
|
|
||||||
const achievements = document.querySelector("#profile-achievements-header");
|
|
||||||
|
|
||||||
games.children[0].addEventListener("click", (clickEvent) => {
|
|
||||||
for (const page of pages) {
|
|
||||||
if (page.id === "games-page") {
|
|
||||||
page.style.display = "block";
|
|
||||||
} else {
|
|
||||||
page.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
achievements.children[0].addEventListener("click", (clickEvent) => {
|
|
||||||
for (page of pages) {
|
|
||||||
if (page.id === "achievements-page") {
|
|
||||||
page.style.display = "block";
|
|
||||||
} else {
|
|
||||||
page.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadFilters = () => {
|
|
||||||
const filters = document.querySelectorAll(".list-page-filter");
|
|
||||||
for (let filter of filters) {
|
|
||||||
filter.addEventListener("click", (clickEvent) => {
|
|
||||||
if (filter.classList.contains("selected")) {
|
|
||||||
filter.classList.remove("selected");
|
|
||||||
} else {
|
|
||||||
filter.classList.add("selected");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", async (loadEvent) => {
|
|
||||||
loadSession();
|
|
||||||
|
|
||||||
await expandTemplates();
|
|
||||||
|
|
||||||
loadPages();
|
|
||||||
|
|
||||||
connectNavbar();
|
|
||||||
connectProfile();
|
|
||||||
|
|
||||||
loadFilters();
|
|
||||||
});
|
|
1
frontend/webpage/static/res/cancel-hover.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M1717.59861,392.24859l-65.17222-65.17219a81.91631,81.91631,0,0,0-115.84716,0L1020.675,842.98065,504.77075,327.07642a81.91631,81.91631,0,0,0-115.84717,0L323.7514,392.2486a81.91631,81.91631,0,0,0,0,115.84716L839.65564,1024,323.75141,1539.90429a81.91631,81.91631,0,0,0,0,115.84717l65.17216,65.17214a81.91632,81.91632,0,0,0,115.84716,0l515.90425-515.9043,515.90425,515.90431a81.91633,81.91633,0,0,0,115.84715,0l65.1722-65.17216a81.91631,81.91631,0,0,0,0-115.84718L1201.69434,1024l515.90428-515.90423A81.91632,81.91632,0,0,0,1717.59861,392.24859Z"/></svg>
|
After Width: | Height: | Size: 716 B |
1
frontend/webpage/static/res/cancel.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M1717.59861,392.24859l-65.17222-65.17219a81.91631,81.91631,0,0,0-115.84716,0L1020.675,842.98065,504.77075,327.07642a81.91631,81.91631,0,0,0-115.84717,0L323.7514,392.2486a81.91631,81.91631,0,0,0,0,115.84716L839.65564,1024,323.75141,1539.90429a81.91631,81.91631,0,0,0,0,115.84717l65.17216,65.17214a81.91632,81.91632,0,0,0,115.84716,0l515.90425-515.9043,515.90425,515.90431a81.91633,81.91633,0,0,0,115.84715,0l65.1722-65.17216a81.91631,81.91631,0,0,0,0-115.84718L1201.69434,1024l515.90428-515.90423A81.91632,81.91632,0,0,0,1717.59861,392.24859Z"/></svg>
|
After Width: | Height: | Size: 716 B |
1
frontend/webpage/static/res/completion.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><style>.cls-1{fill:#44444c;}</style></defs><path class="cls-1" d="M500,0C223.8576,0,0,223.8576,0,500c0,276.14233,223.8576,500,500,500s500-223.85767,500-500C1000,223.8576,776.1424,0,500,0Zm0,955C248.71045,955,45,751.28955,45,500S248.71045,45,500,45,955,248.71045,955,500,751.28955,955,500,955Z"/></svg>
|
After Width: | Height: | Size: 404 B |
1
frontend/webpage/static/res/dropdown-hover.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><polygon class="cls-1" points="1717.02 0 1354.981 0 512 842.981 330.98 1024 1354.98 2048 1717.02 2048 1717.02 1685.961 1055.058 1024 1717.02 362.039 1717.02 0"/></svg>
|
After Width: | Height: | Size: 310 B |
1
frontend/webpage/static/res/dropdown.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><polygon class="cls-1" points="1717.02 0 1354.981 0 512 842.981 330.98 1024 1354.98 2048 1717.02 2048 1717.02 1685.961 1055.058 1024 1717.02 362.039 1717.02 0"/></svg>
|
After Width: | Height: | Size: 310 B |
1
frontend/webpage/static/res/edit-hover.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M1776.471,452.54834l-645.29239,645.29238a56.9113,56.9113,0,0,1-40.24239,16.66895H990.40167a56.91135,56.91135,0,0,1-56.91134-56.91134V957.06378a56.9113,56.9113,0,0,1,16.66895-40.24239L1595.45166,271.529Z"/><path class="cls-1" d="M1534.54675,1056.511v479.48889H512.00012V513.45325H991.48907l256.00007-256.00006H401.27344A145.273,145.273,0,0,0,256.00012,402.72656v1244a145.273,145.273,0,0,0,145.27332,145.27332h1244a145.27292,145.27292,0,0,0,145.27331-145.27332V800.511Z"/><path class="cls-1" d="M1738.98066,53.01934h203.9098a52.0902,52.0902,0,0,1,52.0902,52.0902v151.8196a52.0902,52.0902,0,0,1-52.0902,52.0902h-203.9098a0,0,0,0,1,0,0v-256a0,0,0,0,1,0,0Z" transform="translate(418.82598 1373.17402) rotate(-45)"/></svg>
|
After Width: | Height: | Size: 882 B |
1
frontend/webpage/static/res/edit.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M1776.471,452.54834l-645.29239,645.29238a56.9113,56.9113,0,0,1-40.24239,16.66895H990.40167a56.91135,56.91135,0,0,1-56.91134-56.91134V957.06378a56.9113,56.9113,0,0,1,16.66895-40.24239L1595.45166,271.529Z"/><path class="cls-1" d="M1534.54675,1056.511v479.48889H512.00012V513.45325H991.48907l256.00007-256.00006H401.27344A145.273,145.273,0,0,0,256.00012,402.72656v1244a145.273,145.273,0,0,0,145.27332,145.27332h1244a145.27292,145.27292,0,0,0,145.27331-145.27332V800.511Z"/><path class="cls-1" d="M1738.98066,53.01934h203.9098a52.0902,52.0902,0,0,1,52.0902,52.0902v151.8196a52.0902,52.0902,0,0,1-52.0902,52.0902h-203.9098a0,0,0,0,1,0,0v-256a0,0,0,0,1,0,0Z" transform="translate(418.82598 1373.17402) rotate(-45)"/></svg>
|
After Width: | Height: | Size: 882 B |
1
frontend/webpage/static/res/save-hover.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M170.90729,1032.66212l72.83391-72.83391a76.49864,76.49864,0,0,1,108.18542,0l388.18146,388.18146,955.9653-955.9653a76.49864,76.49864,0,0,1,108.18542,0l72.83391,72.83391a76.49865,76.49865,0,0,1,0,108.18543L740.10808,1710.04834,170.90729,1140.84755A76.49865,76.49865,0,0,1,170.90729,1032.66212Z"/></svg>
|
After Width: | Height: | Size: 466 B |
1
frontend/webpage/static/res/save.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M170.90729,1032.66212l72.83391-72.83391a76.49864,76.49864,0,0,1,108.18542,0l388.18146,388.18146,955.9653-955.9653a76.49864,76.49864,0,0,1,108.18542,0l72.83391,72.83391a76.49865,76.49865,0,0,1,0,108.18543L740.10808,1710.04834,170.90729,1140.84755A76.49865,76.49865,0,0,1,170.90729,1032.66212Z"/></svg>
|
After Width: | Height: | Size: 466 B |
1
frontend/webpage/static/res/upload-hover.svg
Normal file
After Width: | Height: | Size: 19 KiB |
1
frontend/webpage/static/res/upload-invalid.svg
Normal file
After Width: | Height: | Size: 19 KiB |
1
frontend/webpage/static/res/upload.svg
Normal file
After Width: | Height: | Size: 19 KiB |
8
frontend/webpage/static/scripts/about.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
window.addEventListener("load", async (loadEvent) => {
|
||||||
|
await loadCommon();
|
||||||
|
|
||||||
|
await commonTemplates();
|
||||||
|
await template.expand();
|
||||||
|
|
||||||
|
connectNavbar();
|
||||||
|
});
|
97
frontend/webpage/static/scripts/common.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
let root = null;
|
||||||
|
const loadRoot = () => {
|
||||||
|
const rootElement = document.documentElement;
|
||||||
|
|
||||||
|
root = {};
|
||||||
|
root.getProperty = (name) => window.getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||||
|
root.setProperty = (name, value) => {
|
||||||
|
rootElement.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let session = null;
|
||||||
|
const loadSession = async () => {
|
||||||
|
window.addEventListener('beforeunload', (beforeUnloadEvent) => {
|
||||||
|
if (session) {
|
||||||
|
window.sessionStorage.setItem('session', JSON.stringify(session));
|
||||||
|
} else {
|
||||||
|
window.sessionStorage.removeItem('session');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
session = JSON.parse(window.sessionStorage.getItem('session'));
|
||||||
|
if (session) {
|
||||||
|
root.setProperty('--accent-hue', session.hue);
|
||||||
|
|
||||||
|
await fetch(`/api/auth/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key: session.key })
|
||||||
|
})
|
||||||
|
.then(async response => ({ status: response.status, data: await response.json() }))
|
||||||
|
.then(response => {
|
||||||
|
if (response.status !== 200 && window.location.pathname != "/login") {
|
||||||
|
delete session.key;
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCommon = async () => {
|
||||||
|
loadRoot();
|
||||||
|
await loadSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonTemplates = async () => {
|
||||||
|
template.apply("navbar").values([
|
||||||
|
{ section: "left" },
|
||||||
|
{ section: "right" }
|
||||||
|
]);
|
||||||
|
template.apply("navbar-section-left").values([
|
||||||
|
{ item: "project", title: "Project" },
|
||||||
|
{ item: "about", title: "About" }
|
||||||
|
]);
|
||||||
|
if (session) {
|
||||||
|
template.apply("navbar-section-right").values([
|
||||||
|
{ item: "profile", title: "Profile" },
|
||||||
|
{ item: "logout", title: "Logout" }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
template.apply("navbar-section-right").values([
|
||||||
|
{ item: "login", title: "Login" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectNavbar = () => {
|
||||||
|
const navItems = document.querySelectorAll(".navbar-item");
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
24
frontend/webpage/static/scripts/index.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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();
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
window.addEventListener("load", (loadEvent) => {
|
window.addEventListener("load", async (loadEvent) => {
|
||||||
let session = window.sessionStorage.getItem('session');
|
await loadCommon();
|
||||||
if (session) {
|
|
||||||
|
if (session && session.key) {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +19,11 @@ window.addEventListener("load", (loadEvent) => {
|
||||||
const header = document.querySelector("#login-header-text");
|
const header = document.querySelector("#login-header-text");
|
||||||
const error = document.querySelector("#error-message");
|
const error = document.querySelector("#error-message");
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
error.style.display = "block";
|
||||||
|
error.textContent = "You have been signed out due to inactivity";
|
||||||
|
}
|
||||||
|
|
||||||
const raiseError = (errorFields, message) => {
|
const raiseError = (errorFields, message) => {
|
||||||
for (const key in fields) {
|
for (const key in fields) {
|
||||||
if (errorFields.includes(key)) {
|
if (errorFields.includes(key)) {
|
||||||
|
@ -72,7 +78,7 @@ window.addEventListener("load", (loadEvent) => {
|
||||||
raiseError([ "password", "confirm" ], "Password cannot be empty");
|
raiseError([ "password", "confirm" ], "Password cannot be empty");
|
||||||
} else {
|
} else {
|
||||||
freeze();
|
freeze();
|
||||||
fetch('https://localhost:4730/create_user', {
|
fetch(`/api/auth/create_user`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -83,8 +89,8 @@ window.addEventListener("load", (loadEvent) => {
|
||||||
.then(async response => ({ status: response.status, data: await response.json() }))
|
.then(async response => ({ status: response.status, data: await response.json() }))
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
if (response.status === 200) {
|
if (response.status === 201) {
|
||||||
window.sessionStorage.setItem('session', JSON.stringify(data));
|
session = data;
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} else if (response.status === 500) {
|
} else if (response.status === 500) {
|
||||||
raiseError([], "Internal server error :(");
|
raiseError([], "Internal server error :(");
|
||||||
|
@ -95,6 +101,9 @@ window.addEventListener("load", (loadEvent) => {
|
||||||
} else if (data.code === 2) {
|
} else if (data.code === 2) {
|
||||||
raiseError([ "email" ], "Invalid email address");
|
raiseError([ "email" ], "Invalid email address");
|
||||||
fields.email.value = '';
|
fields.email.value = '';
|
||||||
|
} else {
|
||||||
|
raiseError([ "email" ], "Server is bad :p");
|
||||||
|
fields.email.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -130,7 +139,7 @@ window.addEventListener("load", (loadEvent) => {
|
||||||
raiseError([ "password" ], "Password cannot be empty");
|
raiseError([ "password" ], "Password cannot be empty");
|
||||||
} else {
|
} else {
|
||||||
freeze();
|
freeze();
|
||||||
fetch('https://localhost:4730/login', {
|
fetch(`/api/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -142,8 +151,7 @@ window.addEventListener("load", (loadEvent) => {
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
console.log(data);
|
session = data;
|
||||||
window.sessionStorage.setItem('session', JSON.stringify(data));
|
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} else if (response.status === 500) {
|
} else if (response.status === 500) {
|
||||||
raiseError([], "Internal server error :(");
|
raiseError([], "Internal server error :(");
|
278
frontend/webpage/static/scripts/profile.js
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
let profileId = window.location.pathname.split('/').pop();
|
||||||
|
let isReturn = false;
|
||||||
|
let profileData = null;
|
||||||
|
const loadProfile = () => {
|
||||||
|
{
|
||||||
|
const lists = document.querySelectorAll(".profile-list");
|
||||||
|
for (const list of lists) {
|
||||||
|
if (list.querySelectorAll(".profile-entry").length === 0) {
|
||||||
|
list.parentElement.removeChild(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const validImageFile = (type) => {
|
||||||
|
return type === "image/apng"
|
||||||
|
|| type === "image/avif"
|
||||||
|
|| type === "image/gif"
|
||||||
|
|| type === "image/jpeg"
|
||||||
|
|| type === "image/png"
|
||||||
|
|| type === "image/svg+xml"
|
||||||
|
|| type === "image/webp"
|
||||||
|
}
|
||||||
|
|
||||||
|
const editProfileButton = document.querySelector("#info-edit-stack");
|
||||||
|
const saveProfileButton = document.querySelector("#info-save-stack");
|
||||||
|
|
||||||
|
const usernameText = document.querySelector("#profile-info-username-text");
|
||||||
|
const usernameField = document.querySelector("#profile-info-username-field");
|
||||||
|
|
||||||
|
const finalizeName = () => {
|
||||||
|
if (usernameField.value !== '') {
|
||||||
|
fetch(`/api/user/${profileId}/username`, {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: `{ "sessionKey": "${session.key}", "username": "${usernameField.value}" }`
|
||||||
|
}).then(response => {
|
||||||
|
if (response.status === 201) {
|
||||||
|
usernameText.textContent = usernameField.value.substring(0, 32);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
usernameField.addEventListener("input", (inputEvent) => {
|
||||||
|
if (usernameField.value.length > 32) {
|
||||||
|
usernameField.value = usernameField.value.substring(0, 32);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pfp = document.querySelector("#profile-info-pfp-img");
|
||||||
|
const pfpStack = document.querySelector("#profile-info-pfp");
|
||||||
|
const upload = document.querySelector("#profile-info-pfp-upload");
|
||||||
|
const uploadHover = document.querySelector("#profile-info-pfp-upload-hover");
|
||||||
|
const uploadInvalid = document.querySelector("#profile-info-pfp-upload-invalid");
|
||||||
|
|
||||||
|
const togglePlatformEdit = (clickEvent) => {
|
||||||
|
editProfileButton.classList.toggle("active");
|
||||||
|
saveProfileButton.classList.toggle("active");
|
||||||
|
usernameText.classList.toggle("active");
|
||||||
|
usernameField.classList.toggle("active");
|
||||||
|
upload.classList.toggle("active");
|
||||||
|
uploadHover.classList.toggle("active");
|
||||||
|
uploadInvalid.classList.toggle("active");
|
||||||
|
pfpStack.classList.remove("hover");
|
||||||
|
pfpStack.classList.remove("invalid");
|
||||||
|
};
|
||||||
|
editProfileButton.addEventListener("click", togglePlatformEdit);
|
||||||
|
editProfileButton.addEventListener("click", () => {
|
||||||
|
usernameField.value = usernameText.textContent;
|
||||||
|
});
|
||||||
|
saveProfileButton.addEventListener("click", togglePlatformEdit);
|
||||||
|
saveProfileButton.addEventListener("click", finalizeName);
|
||||||
|
|
||||||
|
pfpStack.addEventListener("drop", (dropEvent) => {
|
||||||
|
if (upload.classList.contains("active")) {
|
||||||
|
dropEvent.preventDefault();
|
||||||
|
pfpStack.classList.remove("hover");
|
||||||
|
pfpStack.classList.remove("invalid");
|
||||||
|
if (dropEvent.dataTransfer.files) {
|
||||||
|
const file = dropEvent.dataTransfer.files[0];
|
||||||
|
if (validImageFile(file.type)) {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('session', new Blob([`{ "key": "${session.key}" }`], { type: `application/json` }));
|
||||||
|
data.append('file', file);
|
||||||
|
|
||||||
|
fetch(`/api/user/${profileId}/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
body: data
|
||||||
|
}).then(response => {
|
||||||
|
if (upload.classList.contains("active")) {
|
||||||
|
if (response.status === 201) {
|
||||||
|
pfp.src = `/api/user/${profileId}/image?time=${Date.now()}`;
|
||||||
|
} else {
|
||||||
|
pfpStack.classList.add("invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pfpStack.classList.add("invalid");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pfpStack.addEventListener("dragover", (dragEvent) => {
|
||||||
|
if (upload.classList.contains("active")) {
|
||||||
|
dragEvent.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pfpStack.addEventListener("dragenter", (dragEvent) => {
|
||||||
|
if (upload.classList.contains("active")) {
|
||||||
|
pfpStack.classList.remove("hover");
|
||||||
|
pfpStack.classList.remove("invalid");
|
||||||
|
if (dragEvent.dataTransfer.types.includes("application/x-moz-file")) {
|
||||||
|
pfpStack.classList.add("hover");
|
||||||
|
} else {
|
||||||
|
pfpStack.classList.add("invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pfpStack.addEventListener("dragleave", (dragEvent) => {
|
||||||
|
if (upload.classList.contains("active")) {
|
||||||
|
pfpStack.classList.remove("hover");
|
||||||
|
pfpStack.classList.remove("invalid");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const editPlatformsButton = document.querySelector("#platform-edit-stack");
|
||||||
|
const savePlatformsButton = document.querySelector("#platform-save-stack");
|
||||||
|
const platforms = document.querySelectorAll("#profile-platforms .profile-entry");
|
||||||
|
|
||||||
|
const togglePlatformEdit = (clickEvent) => {
|
||||||
|
editPlatformsButton.classList.toggle("active");
|
||||||
|
savePlatformsButton.classList.toggle("active");
|
||||||
|
for (const platform of platforms) {
|
||||||
|
platform.classList.toggle("editing");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
editPlatformsButton.addEventListener("click", togglePlatformEdit);
|
||||||
|
savePlatformsButton.addEventListener("click", togglePlatformEdit);
|
||||||
|
|
||||||
|
const steamButtons = [
|
||||||
|
document.querySelector("#add-steam"),
|
||||||
|
document.querySelector("#platform-0"),
|
||||||
|
];
|
||||||
|
|
||||||
|
steamButtons[0].addEventListener("click", (clickEvent) => {
|
||||||
|
window.location.href = "/auth/steam";
|
||||||
|
});
|
||||||
|
steamButtons[1].addEventListener("click", (clickEvent) => {
|
||||||
|
fetch(`/api/user/${profileId}/platforms/remove`, {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ sessionKey: session.key, platformId: 0 })
|
||||||
|
});
|
||||||
|
steamButtons[1].parentElement.classList.remove("connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isReturn) {
|
||||||
|
editPlatformsButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvasing
|
||||||
|
|
||||||
|
const completionCanvas = document.querySelector("#profile-completion-canvas");
|
||||||
|
const completionText = document.querySelector("#profile-completion-text");
|
||||||
|
|
||||||
|
const STROKE_WIDTH = 0.18;
|
||||||
|
const style = window.getComputedStyle(completionCanvas);
|
||||||
|
const context = completionCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const drawCanvas = () => profileData.then(data => {
|
||||||
|
const width = Number(style.getPropertyValue('width').slice(0, -2));
|
||||||
|
const height = width;
|
||||||
|
|
||||||
|
context.canvas.width = width;
|
||||||
|
context.canvas.height = height;
|
||||||
|
context.clearRect(0, 0, width, height);
|
||||||
|
context.strokeStyle = root.getProperty('--accent-value3');
|
||||||
|
context.lineWidth = (width / 2) * STROKE_WIDTH;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(width / 2, height / 2, (width / 2) * (1 - STROKE_WIDTH / 2), -0.5 * Math.PI, (-0.5 + (data.average === null ? 0 : (data.average / 100) * 2)) * Math.PI);
|
||||||
|
context.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', drawCanvas);
|
||||||
|
drawCanvas();
|
||||||
|
|
||||||
|
if (profileId === session.id) {
|
||||||
|
document.querySelector("#profile-page").classList.add("self");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandTemplates = async () => {
|
||||||
|
await commonTemplates();
|
||||||
|
template.apply("profile-page").promise(profileData.then(data => ({
|
||||||
|
id: profileId,
|
||||||
|
username: data.username,
|
||||||
|
completed: data.completed,
|
||||||
|
average: data.average === null ? "N/A" : data.average + "%",
|
||||||
|
perfect: data.perfect,
|
||||||
|
})));
|
||||||
|
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" />`,
|
||||||
|
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 === 1 ? `<p class="platform-unsupported">Coming soon...</p>` :
|
||||||
|
(platform.id === 2 ? `<p class="platform-unsupported">Coming soon...</p>` :
|
||||||
|
"")))
|
||||||
|
}))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", async (loadEvent) => {
|
||||||
|
await loadCommon();
|
||||||
|
|
||||||
|
if (!/\d+/.test(profileId)) {
|
||||||
|
isReturn = true;
|
||||||
|
const platform = profileId;
|
||||||
|
if (!session) {
|
||||||
|
window.location.href = "/404";
|
||||||
|
} else {
|
||||||
|
profileId = session.lastProfile;
|
||||||
|
delete session.lastProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'steam') {
|
||||||
|
const query = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
if (query.get('openid.mode') === 'cancel') {
|
||||||
|
|
||||||
|
} 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", {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ sessionKey: session.key, userId: profileId, platformId: 0, platformUserId: `${steamId}` })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.history.replaceState({}, '', `/profile/${profileId}`);
|
||||||
|
} else if (/\d+/.test(profileId)) {
|
||||||
|
profileId = Number(profileId);
|
||||||
|
if (session) {
|
||||||
|
session.lastProfile = profileId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
profileData = fetch(`/api/user/${profileId}`, { method: 'GET', mode: 'cors' })
|
||||||
|
.then(response => response.json());
|
||||||
|
|
||||||
|
await expandTemplates();
|
||||||
|
await template.expand();
|
||||||
|
|
||||||
|
connectNavbar();
|
||||||
|
loadProfile();
|
||||||
|
});
|
|
@ -151,7 +151,7 @@ var template = template || {};
|
||||||
const data = child.dataset.template.split(/\s*:\s*/);
|
const data = child.dataset.template.split(/\s*:\s*/);
|
||||||
return {
|
return {
|
||||||
id: data[0],
|
id: data[0],
|
||||||
typeCapture: parseType(data[1] || 'Begin'),
|
typeCapture: parseType(data[1] || 'Basic'),
|
||||||
element: child
|
element: child
|
||||||
};
|
};
|
||||||
});
|
});
|
13
frontend/webpage/static/styles/about.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#about-page {
|
||||||
|
max-width: 1600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#about-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
background-color: var(--distinction);
|
||||||
|
}
|
|
@ -134,8 +134,6 @@ html, body {
|
||||||
|
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
box-shadow: 0px 0px 5px 10px var(--shadow-color);
|
box-shadow: 0px 0px 5px 10px var(--shadow-color);
|
||||||
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subsection {
|
.page-subsection {
|
||||||
|
@ -171,17 +169,28 @@ html, body {
|
||||||
|
|
||||||
.page-subheader {
|
.page-subheader {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subheader-flex {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header-text,
|
.page-header-text,
|
||||||
.page-subheader-text {
|
.page-subheader-text {
|
||||||
width: max-content;
|
|
||||||
|
|
||||||
margin: 0 0 0.25em;
|
margin: 0 0 0.25em;
|
||||||
|
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header-text.link,
|
.page-header-text.link,
|
||||||
|
@ -199,10 +208,17 @@ html, body {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subheader-text {
|
.page-subheader-text,
|
||||||
|
.page-subheader-icon {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-subheader-icon {
|
||||||
|
margin: 0 0 0.25em;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.page-header-separator,
|
.page-header-separator,
|
||||||
.page-subheader-separator {
|
.page-subheader-separator {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -214,8 +230,6 @@ html, body {
|
||||||
.list-page-search {
|
.list-page-search {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
margin: 16px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -285,6 +299,9 @@ html, body {
|
||||||
|
|
||||||
.list-page-filter-chunk {
|
.list-page-filter-chunk {
|
||||||
background-color: var(--distinction);
|
background-color: var(--distinction);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-page-filter {
|
.list-page-filter {
|
||||||
|
@ -394,7 +411,6 @@ html, body {
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
width: 0;
|
|
||||||
height: 64px;
|
height: 64px;
|
||||||
line-height: 64px;
|
line-height: 64px;
|
||||||
|
|
111
frontend/webpage/static/styles/index.css
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
#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;
|
||||||
|
}
|
401
frontend/webpage/static/styles/profile.css
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
#profile-page {
|
||||||
|
max-width: 1600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-list {
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
height: 64px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-color: var(--distinction);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry-left {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry-right {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry-icon {
|
||||||
|
width: 64px;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry-text {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0px 16px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
line-height: 64px;
|
||||||
|
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
border-top: 1px solid var(--background);
|
||||||
|
|
||||||
|
flex-basis: max-content;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top > .profile-entry-text {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry-text.platform-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-section-1 {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info {
|
||||||
|
width: 50%;
|
||||||
|
height: max-content;
|
||||||
|
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-username-text.active,
|
||||||
|
#profile-info-username-field {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-username-field.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-username-field {
|
||||||
|
margin-right: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--background);
|
||||||
|
background-color: var(--foreground);
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp-border {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
padding: 24px;
|
||||||
|
background-color: var(--distinction);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
|
||||||
|
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp-img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
background-color: var(--background);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp-vignette {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
box-shadow: inset 0px 0px 30px 10px var(--shadow-color);
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp-upload,
|
||||||
|
#profile-info-pfp-upload-hover,
|
||||||
|
#profile-info-pfp-upload-invalid {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
background-color: var(--background);
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp-upload {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp-upload-hover,
|
||||||
|
#profile-info-pfp-upload-invalid {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp #profile-info-pfp-upload.active,
|
||||||
|
#profile-info-pfp.hover #profile-info-pfp-upload-hover.active,
|
||||||
|
#profile-info-pfp.invalid #profile-info-pfp-upload-invalid.active {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-info-pfp.hover #profile-info-pfp-upload.active,
|
||||||
|
#profile-info-pfp.invalid #profile-info-pfp-upload.active {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-stats {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-stats-numeric {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-completion-stack {
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-completion-background {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-completion-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-completion-text {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 64px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-perfect-text {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
font-size: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-section-2 {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-hardest {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-platforms {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-platforms .profile-entry {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-platforms .profile-entry-text {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-platforms .profile-entry.connected,
|
||||||
|
#profile-platforms .profile-entry.editing {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-page .profile-edit-stack,
|
||||||
|
#profile-page .profile-save-stack {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-page.self .profile-edit-stack,
|
||||||
|
#profile-page.self .profile-save-stack.active {
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
|
height: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-page.self .profile-edit-stack.active,
|
||||||
|
#profile-page.self .profile-save-stack {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-edit-stack:hover > .profile-edit-hover,
|
||||||
|
.profile-edit {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-edit-stack:hover > .profile-edit,
|
||||||
|
.profile-edit-hover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-save-stack:hover > .profile-save-hover,
|
||||||
|
.profile-save {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-save-stack:hover > .profile-save,
|
||||||
|
.profile-save-hover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry .platform-remove-stack,
|
||||||
|
.profile-entry .platform-add,
|
||||||
|
.profile-entry .platform-unsupported,
|
||||||
|
.profile-entry.connected.editing .platform-add {
|
||||||
|
border-top: 1px solid var(--background);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry.connected.editing .platform-remove-stack {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: max-content;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-remove, .platform-remove-hover {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry.connected.editing .platform-remove-stack .platform-remove,
|
||||||
|
.profile-entry.connected.editing .platform-remove-stack:hover .platform-remove-hover {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry.connected.editing .platform-remove-stack:hover .platform-remove,
|
||||||
|
.profile-entry.connected.editing .platform-remove-stack .platform-remove-hover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry .platform-add {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry.editing .platform-add {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-entry.editing .platform-unsupported {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0% 2%;
|
||||||
|
line-height: 63px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--foreground-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-ratings {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
:root {
|
:root {
|
||||||
--background-dark: #111117;
|
--background-dark: #111117;
|
||||||
--background: #22222A;
|
--background: #22222A;
|
||||||
|
--foreground-disabled: #77777D;
|
||||||
--foreground-dark: #AAAAAA;
|
--foreground-dark: #AAAAAA;
|
||||||
--foreground: #EEEEEE;
|
--foreground: #EEEEEE;
|
||||||
--distinction: #44444C;
|
--distinction: #44444C;
|
|
@ -1,232 +0,0 @@
|
||||||
html, body {
|
|
||||||
background-color: var(--background-dark);
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#games-page {
|
|
||||||
max-width: 1920px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-page-entry-text.game-name {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-page-entry-text.game-description {
|
|
||||||
flex-grow: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#achievements-page {
|
|
||||||
max-width: 1920px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-page-entry-text.achievement-name {
|
|
||||||
flex-grow: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-page-entry-text.achievement-description {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 12px;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
flex-grow: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-page-entry-text.achievement-stages {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-page {
|
|
||||||
max-width: 1600px;
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-section-1 {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-info {
|
|
||||||
width: 50%;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-info-pfp-border {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
padding: 24px;
|
|
||||||
background-color: var(--distinction);
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
max-width: 640px;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-info-pfp {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-info-pfp-vignette {
|
|
||||||
top: 0%;
|
|
||||||
left: 0%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
box-shadow: inset 0px 0px 50px 30px var(--shadow-color);
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-info-pfp-img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-list {
|
|
||||||
width: 100%;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
height: 64px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background-color: var(--distinction);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry.accented {
|
|
||||||
background-color: var(--accent-value1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry-left {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry-right {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry-icon {
|
|
||||||
width: 64px;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry-text {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
padding: 0px 16px;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
line-height: 64px;
|
|
||||||
|
|
||||||
color: var(--foreground);
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
|
|
||||||
border-top: 1px solid var(--background);
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top > .profile-entry-text {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accented > .profile-entry-text {
|
|
||||||
border-color: var(--accent-value0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry-text.platform-name,
|
|
||||||
.profile-entry-text.game-name,
|
|
||||||
.profile-entry-text.achievement-name {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry-text.accented {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-entry.accented .profile-entry-text.accented {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-platforms {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-section-2 {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#profile-games,
|
|
||||||
#profile-achievements {
|
|
||||||
width: 50%;
|
|
||||||
height: max-content;
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
<div class="page-subsection">
|
|
||||||
<div 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 class="list-page-partitions">
|
|
||||||
<div class="list-page-filter-partition">
|
|
||||||
<div class="page-subsection-wrapper">
|
|
||||||
<div class="page-subheader">
|
|
||||||
<p class="page-subheader-text">Filters</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">My Games</p>
|
|
||||||
</div>
|
|
||||||
<div id="in-progress-filter" class="list-page-filter">
|
|
||||||
<div class="list-page-filter-checkbox"></div>
|
|
||||||
<p class="list-page-filter-name">In Progress</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>
|
|
||||||
<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="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>
|
|
|
@ -1,65 +0,0 @@
|
||||||
<div class="page-subsection">
|
|
||||||
<div class="list-page-search page-subsection-chunk">
|
|
||||||
<label for="game-search">Search</label>
|
|
||||||
<input id="game-search" type="text" placeholder="Name" game-name="game-search" name="game-search"/>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-partitions">
|
|
||||||
<div class="list-page-filter-partition">
|
|
||||||
<div class="page-subsection-wrapper">
|
|
||||||
<div class="page-subheader">
|
|
||||||
<p class="page-subheader-text">Filters</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 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 game-name">Name</p>
|
|
||||||
<p class="list-page-entry-text game-description">Description</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-entry">
|
|
||||||
<img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
|
|
||||||
<p class="list-page-entry-text game-name">Latin</p>
|
|
||||||
<p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-entry">
|
|
||||||
<img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
|
|
||||||
<p class="list-page-entry-text game-name">Latin</p>
|
|
||||||
<p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-entry">
|
|
||||||
<img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
|
|
||||||
<p class="list-page-entry-text game-name">Latin</p>
|
|
||||||
<p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-entry">
|
|
||||||
<img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
|
|
||||||
<p class="list-page-entry-text game-name">Latin</p>
|
|
||||||
<p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-entry">
|
|
||||||
<img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
|
|
||||||
<p class="list-page-entry-text game-name">Latin</p>
|
|
||||||
<p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-page-entry">
|
|
||||||
<img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
|
|
||||||
<p class="list-page-entry-text game-name">Latin</p>
|
|
||||||
<p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,172 +0,0 @@
|
||||||
<div id="profile-section-1">
|
|
||||||
<div id="profile-info" class="page-subsection">
|
|
||||||
<div class="page-subsection-wrapper">
|
|
||||||
<div class="page-subheader">
|
|
||||||
<p id="profile-info-name" class="page-subheader-text">Jane Doe</p>
|
|
||||||
<div class="page-subheader-separator"></div>
|
|
||||||
</div>
|
|
||||||
<div id="profile-info-pfp-border" class="page-subsection-chunk">
|
|
||||||
<div id="profile-info-pfp">
|
|
||||||
<img id="profile-info-pfp-img" src="res/guest_pfp.png" alt="User's Profile Pictuer" />
|
|
||||||
<div id="profile-info-pfp-vignette"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="profile-platforms" class="page-subsection">
|
|
||||||
<div class="page-subsection-wrapper">
|
|
||||||
<div class="page-subheader">
|
|
||||||
<p class="page-subheader-text">Platforms</p>
|
|
||||||
<div class="page-subheader-separator"></div>
|
|
||||||
</div>
|
|
||||||
<div class="profile-list page-subsection-chunk">
|
|
||||||
<div class="profile-entry accented top">
|
|
||||||
<img class="profile-entry-icon" src="res/steam.png" alt="Steam Logo" />
|
|
||||||
<p class="profile-entry-text platform-name">Steam</p>
|
|
||||||
<p class="profile-entry-text accented">Connected</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/xbox.png" alt="Xbox Logo" />
|
|
||||||
<p class="profile-entry-text platform-name">Xbox Live</p>
|
|
||||||
<p class="profile-entry-text accented">Connected</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/psn.png" alt="PSN Logo" />
|
|
||||||
<p class="profile-entry-text platform-name">PSN</p>
|
|
||||||
<p class="profile-entry-text accented">Connected</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="profile-section-2">
|
|
||||||
<div id="profile-games" class="page-subsection">
|
|
||||||
<div class="page-subsection-wrapper">
|
|
||||||
<div id="profile-games-header" class="page-subheader">
|
|
||||||
<p class="page-subheader-text link">Games</p>
|
|
||||||
<div class="page-subheader-separator"></div>
|
|
||||||
</div>
|
|
||||||
<div class="profile-list page-subsection-chunk">
|
|
||||||
<div class="profile-entry game top">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry game">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
|
|
||||||
<p class="profile-entry-text game-name">Latin</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="profile-achievements" class="page-subsection">
|
|
||||||
<div class="page-subsection-wrapper">
|
|
||||||
<div id="profile-achievements-header" class="page-subheader">
|
|
||||||
<p class="page-subheader-text link">Achievements</p>
|
|
||||||
<div class="page-subheader-separator"></div>
|
|
||||||
</div>
|
|
||||||
<div class="profile-list page-subsection-chunk">
|
|
||||||
<div class="profile-entry accented top">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry accented">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry accented">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry accented">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry accented">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry accented">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry accented">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-entry">
|
|
||||||
<img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
|
|
||||||
<p class="profile-entry-text achievement-name">Lorem Ipsum</p>
|
|
||||||
<p class="profile-entry-text accented">Completed</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -36,6 +36,7 @@ CREATE TABLE [User] (
|
||||||
Hue INT NOT NULL
|
Hue INT NOT NULL
|
||||||
CONSTRAINT HueDefault DEFAULT 0
|
CONSTRAINT HueDefault DEFAULT 0
|
||||||
CONSTRAINT HueConstraint CHECK (0 <= Hue AND Hue <= 360),
|
CONSTRAINT HueConstraint CHECK (0 <= Hue AND Hue <= 360),
|
||||||
|
PFP VARCHAR(11) NULL,
|
||||||
Verified BIT NOT NULL
|
Verified BIT NOT NULL
|
||||||
CONSTRAINT VerifiedDefault DEFAULT 0
|
CONSTRAINT VerifiedDefault DEFAULT 0
|
||||||
PRIMARY KEY(ID)
|
PRIMARY KEY(ID)
|
||||||
|
@ -102,7 +103,8 @@ CREATE TABLE [Progress] (
|
||||||
|
|
||||||
CREATE TABLE [IsOn] (
|
CREATE TABLE [IsOn] (
|
||||||
UserID INT NOT NULL,
|
UserID INT NOT NULL,
|
||||||
PlatformID INT NOT NULL
|
PlatformID INT NOT NULL,
|
||||||
|
PlatformUserID VARCHAR(32) NOT NULL
|
||||||
PRIMARY KEY(UserID, PlatformID)
|
PRIMARY KEY(UserID, PlatformID)
|
||||||
FOREIGN KEY(UserID) REFERENCES [User](ID)
|
FOREIGN KEY(UserID) REFERENCES [User](ID)
|
||||||
ON UPDATE CASCADE
|
ON UPDATE CASCADE
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
CREATE PROCEDURE GetUserLogin(
|
|
||||||
@email VARCHAR(254)
|
|
||||||
) AS
|
|
||||||
BEGIN TRANSACTION
|
|
||||||
SELECT Id, Salt, [Password], Hue FROM [User] WHERE Email = @email
|
|
||||||
COMMIT TRANSACTION
|
|
186
sql/UserData.sql
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
---------------------------------------
|
||||||
|
-- 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
|
BIN
tmp/unknown.png
Before Width: | Height: | Size: 92 KiB |