add bot detail page
This commit is contained in:
parent
33664eff2c
commit
ccfe86729e
8 changed files with 185 additions and 104 deletions
|
@ -57,6 +57,12 @@ pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResul
|
||||||
.get_result::<User>(conn)
|
.get_result::<User>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_user(user_id: i32, db_conn: &PgConnection) -> QueryResult<User> {
|
||||||
|
users::table
|
||||||
|
.filter(users::id.eq(user_id))
|
||||||
|
.first::<User>(db_conn)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_user_by_name(username: &str, db_conn: &PgConnection) -> QueryResult<User> {
|
pub fn find_user_by_name(username: &str, db_conn: &PgConnection) -> QueryResult<User> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::username.eq(username))
|
.filter(users::username.eq(username))
|
||||||
|
|
|
@ -124,9 +124,9 @@ pub fn api() -> Router {
|
||||||
"/bots",
|
"/bots",
|
||||||
get(routes::bots::list_bots).post(routes::bots::create_bot),
|
get(routes::bots::list_bots).post(routes::bots::create_bot),
|
||||||
)
|
)
|
||||||
.route("/bots/:bot_id", get(routes::bots::get_bot))
|
.route("/bots/:bot_name", get(routes::bots::get_bot))
|
||||||
.route(
|
.route(
|
||||||
"/bots/:bot_id/upload",
|
"/bots/:bot_name/upload",
|
||||||
post(routes::bots::upload_code_multipart),
|
post(routes::bots::upload_code_multipart),
|
||||||
)
|
)
|
||||||
.route("/matches", get(routes::matches::list_matches))
|
.route("/matches", get(routes::matches::list_matches))
|
||||||
|
|
|
@ -20,6 +20,8 @@ use crate::modules::bots::save_code_string;
|
||||||
use crate::{DatabaseConnection, GlobalConfig};
|
use crate::{DatabaseConnection, GlobalConfig};
|
||||||
use bots::Bot;
|
use bots::Bot;
|
||||||
|
|
||||||
|
use super::users::UserData;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct SaveBotParams {
|
pub struct SaveBotParams {
|
||||||
pub bot_name: String,
|
pub bot_name: String,
|
||||||
|
@ -148,14 +150,23 @@ pub async fn create_bot(
|
||||||
// TODO: handle errors
|
// TODO: handle errors
|
||||||
pub async fn get_bot(
|
pub async fn get_bot(
|
||||||
conn: DatabaseConnection,
|
conn: DatabaseConnection,
|
||||||
Path(bot_id): Path<i32>,
|
Path(bot_name): Path<String>,
|
||||||
) -> Result<Json<JsonValue>, StatusCode> {
|
) -> Result<Json<JsonValue>, StatusCode> {
|
||||||
let bot = bots::find_bot(bot_id, &conn).map_err(|_| StatusCode::NOT_FOUND)?;
|
let bot = db::bots::find_bot_by_name(&bot_name, &conn).map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
let bundles =
|
let owner: Option<UserData> = match bot.owner_id {
|
||||||
|
Some(user_id) => {
|
||||||
|
let user = db::users::find_user(user_id, &conn)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
Some(user.into())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let versions =
|
||||||
bots::find_bot_versions(bot.id, &conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
bots::find_bot_versions(bot.id, &conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"bot": bot,
|
"bot": bot,
|
||||||
"bundles": bundles,
|
"owner": owner,
|
||||||
|
"versions": versions,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,13 +198,13 @@ pub async fn get_ranking(conn: DatabaseConnection) -> Result<Json<Vec<RankedBot>
|
||||||
pub async fn upload_code_multipart(
|
pub async fn upload_code_multipart(
|
||||||
conn: DatabaseConnection,
|
conn: DatabaseConnection,
|
||||||
user: User,
|
user: User,
|
||||||
Path(bot_id): Path<i32>,
|
Path(bot_name): Path<String>,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<Json<BotVersion>, StatusCode> {
|
) -> Result<Json<BotVersion>, StatusCode> {
|
||||||
let bots_dir = PathBuf::from(&config.bots_directory);
|
let bots_dir = PathBuf::from(&config.bots_directory);
|
||||||
|
|
||||||
let bot = bots::find_bot(bot_id, &conn).map_err(|_| StatusCode::NOT_FOUND)?;
|
let bot = bots::find_bot_by_name(&bot_name, &conn).map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
if Some(user.id) != bot.owner_id {
|
if Some(user.id) != bot.owner_id {
|
||||||
return Err(StatusCode::FORBIDDEN);
|
return Err(StatusCode::FORBIDDEN);
|
||||||
|
|
|
@ -41,11 +41,17 @@
|
||||||
<td class="leaderboard-rating">
|
<td class="leaderboard-rating">
|
||||||
{formatRating(entry)}
|
{formatRating(entry)}
|
||||||
</td>
|
</td>
|
||||||
<td class="leaderboard-bot">{entry["bot"]["name"]}</td>
|
<td class="leaderboard-bot">
|
||||||
|
<a class="leaderboard-href" href="/bots/{entry['bot']['name']}"
|
||||||
|
>{entry["bot"]["name"]}
|
||||||
|
</a></td
|
||||||
|
>
|
||||||
<td class="leaderboard-author">
|
<td class="leaderboard-author">
|
||||||
{#if entry["author"]}
|
{#if entry["author"]}
|
||||||
<!-- TODO: remove duplication -->
|
<!-- TODO: remove duplication -->
|
||||||
<a href="/users/{entry["author"]["username"]}">{entry["author"]["username"]}</a>
|
<a class="leaderboard-href" href="/users/{entry['author']['username']}"
|
||||||
|
>{entry["author"]["username"]}</a
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -71,7 +77,7 @@
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-author a{
|
.leaderboard-href {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,8 @@
|
||||||
|
|
||||||
<div class="user-controls">
|
<div class="user-controls">
|
||||||
{#if $currentUser}
|
{#if $currentUser}
|
||||||
<a class="current-user-name" href="/users/{$currentUser["username"]}">
|
<a class="current-user-name" href="/users/{$currentUser['username']}">
|
||||||
{$currentUser["username"]}
|
{$currentUser["username"]}
|
||||||
</a>
|
</a>
|
||||||
<div class="sign-out" on:click={signOut}>Sign out</div>
|
<div class="sign-out" on:click={signOut}>Sign out</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
<script lang="ts" context="module">
|
|
||||||
import { get_session_token } from "$lib/auth";
|
|
||||||
|
|
||||||
export async function load({ page }) {
|
|
||||||
const token = get_session_token();
|
|
||||||
const res = await fetch(`/api/bots/${page.params["bot_id"]}`, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
bot: data["bot"],
|
|
||||||
bundles: data["bundles"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: res.status,
|
|
||||||
error: new Error("Could not load bot"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
export let bot: object;
|
|
||||||
export let bundles: object[];
|
|
||||||
|
|
||||||
let files;
|
|
||||||
|
|
||||||
async function submitCode() {
|
|
||||||
console.log("click");
|
|
||||||
const token = get_session_token();
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("File", files[0]);
|
|
||||||
|
|
||||||
const res = await fetch(`/api/bots/${bot["id"]}/upload`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
// the content type header will be set by the browser
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(res.statusText);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{bot["name"]}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Upload code</div>
|
|
||||||
<form on:submit|preventDefault={submitCode}>
|
|
||||||
<input type="file" bind:files />
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{#each bundles as bundle}
|
|
||||||
<li>
|
|
||||||
bundle created at {dayjs(bundle["created_at"]).format("YYYY-MM-DD HH:mm")}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
139
web/pw-server/src/routes/bots/[bot_name].svelte
Normal file
139
web/pw-server/src/routes/bots/[bot_name].svelte
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import { get_session_token } from "$lib/auth";
|
||||||
|
|
||||||
|
export async function load({ params, fetch }) {
|
||||||
|
const token = get_session_token();
|
||||||
|
const res = await fetch(`/api/bots/${params["bot_name"]}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const { bot, owner, versions } = await res.json();
|
||||||
|
// sort most recent first
|
||||||
|
versions.sort((a: string, b: string) =>
|
||||||
|
dayjs(a["created_at"]).isAfter(b["created_at"]) ? -1 : 1
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
bot,
|
||||||
|
owner,
|
||||||
|
versions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
error: new Error("Could not find bot"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { currentUser } from "$lib/stores/current_user";
|
||||||
|
|
||||||
|
export let bot: object;
|
||||||
|
export let owner: object;
|
||||||
|
export let versions: object[];
|
||||||
|
|
||||||
|
// function last_updated() {
|
||||||
|
// versions.sort()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let files;
|
||||||
|
|
||||||
|
// async function submitCode() {
|
||||||
|
// console.log("click");
|
||||||
|
// const token = get_session_token();
|
||||||
|
|
||||||
|
// const formData = new FormData();
|
||||||
|
// formData.append("File", files[0]);
|
||||||
|
|
||||||
|
// const res = await fetch(`/api/bots/${bot["id"]}/upload`, {
|
||||||
|
// method: "POST",
|
||||||
|
// headers: {
|
||||||
|
// // the content type header will be set by the browser
|
||||||
|
// Authorization: `Bearer ${token}`,
|
||||||
|
// },
|
||||||
|
// body: formData,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// console.log(res.statusText);
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div>Upload code</div>
|
||||||
|
<form on:submit|preventDefault={submitCode}>
|
||||||
|
<input type="file" bind:files />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form> -->
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="bot-name">{bot["name"]}</h1>
|
||||||
|
{#if owner}
|
||||||
|
<a class="owner-name" href="/users/{owner['username']}">
|
||||||
|
{owner["username"]}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $currentUser && $currentUser["user_id"] === bot["owner_id"]}
|
||||||
|
<div>
|
||||||
|
<!-- TODO: can we avoid hardcoding the url? -->
|
||||||
|
Publish a new version by pushing a docker container to
|
||||||
|
<code>registry.planetwars.dev/{bot["name"]}:latest</code>, or using the web editor.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="versions">
|
||||||
|
<h4>Versions</h4>
|
||||||
|
<ul class="version-list">
|
||||||
|
{#each versions as version}
|
||||||
|
<li>
|
||||||
|
{dayjs(version["created_at"]).format("YYYY-MM-DD HH:mm")}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.container {
|
||||||
|
width: 800px;
|
||||||
|
max-width: 80%;
|
||||||
|
margin: 50px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
$header-space-above-line: 12px;
|
||||||
|
|
||||||
|
.bot-name {
|
||||||
|
font-size: 24pt;
|
||||||
|
margin-bottom: $header-space-above-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-name {
|
||||||
|
font-size: 14pt;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: $header-space-above-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,4 @@
|
||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
function fetchJson(url: string): Promise<Response> {
|
|
||||||
return fetch(url, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load({ params, fetch }) {
|
export async function load({ params, fetch }) {
|
||||||
const userName = params["user_name"];
|
const userName = params["user_name"];
|
||||||
const userBotsResponse = await fetch(`/api/users/${userName}/bots`);
|
const userBotsResponse = await fetch(`/api/users/${userName}/bots`);
|
||||||
|
@ -36,17 +28,17 @@
|
||||||
<h2>Bots</h2>
|
<h2>Bots</h2>
|
||||||
<ul class="bot-list">
|
<ul class="bot-list">
|
||||||
{#each bots as bot}
|
{#each bots as bot}
|
||||||
<li class="bot">
|
<li class="bot">
|
||||||
<span class="bot-name">{bot['name']}</span>
|
<a class="bot-name" href="/bots/{bot['name']}">{bot["name"]}</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.container {
|
.container {
|
||||||
min-width: 600px;
|
width: 800px;
|
||||||
max-width: 800px;
|
max-width: 80%;
|
||||||
margin: 50px auto;
|
margin: 50px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +48,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
margin-bottom: .5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-list {
|
.bot-list {
|
||||||
|
@ -75,10 +67,11 @@
|
||||||
.bot-name {
|
.bot-name {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot:first-child {
|
.bot:first-child {
|
||||||
border-top: 1px solid $border-color;
|
border-top: 1px solid $border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
Loading…
Reference in a new issue