add bot detail page

This commit is contained in:
Ilion Beyst 2022-07-24 16:45:29 +02:00
parent 33664eff2c
commit ccfe86729e
8 changed files with 185 additions and 104 deletions

View file

@ -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))

View file

@ -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))

View file

@ -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);

View file

@ -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;
} }

View file

@ -36,7 +36,7 @@
<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>

View file

@ -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>

View 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>

View file

@ -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`);
@ -37,7 +29,7 @@
<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>
@ -45,8 +37,8 @@
<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>