mirror of
https://github.com/psforever/PSFPortal.git
synced 2026-07-15 00:14:37 +00:00
Initial commit
This commit is contained in:
commit
20946fdb43
49 changed files with 2393 additions and 0 deletions
70
app/views/AdminPanel.svelte
Normal file
70
app/views/AdminPanel.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import UserList from '../views/UserList'
|
||||
import CharacterList from '../views/CharacterList'
|
||||
import CharacterLink from '../components/CharacterLink'
|
||||
import AccountLink from '../components/AccountLink'
|
||||
import axios from 'axios'
|
||||
export let appAlert;
|
||||
|
||||
let results;
|
||||
async function submitSearch(event) {
|
||||
const value = event.target.search.value;
|
||||
|
||||
try {
|
||||
const resp = await axios.post("/api/search", { search : value })
|
||||
results = resp.data.items;
|
||||
} catch (e) {
|
||||
appAlert.message(e.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>PSForever - Admin Panel</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Admin Panel</h1>
|
||||
|
||||
<!--<strong>Last account created:</strong> <AccountLink account={stats.last.account} /> (<span title={moment(stats.last.account.created).format(`MMMM Do YYYY, h:mm:ss a`)}>{moment(stats.last.account.created).fromNow()}</span>)<br/>-->
|
||||
|
||||
<ul class="nav nav-tabs mb-3" id="nav-tab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="search-tab" data-toggle="tab" href="#search" role="tab" aria-controls="search" aria-selected="true">Search</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="users-tab" data-toggle="tab" href="#users" role="tab" aria-controls="home" aria-selected="false">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="characters-tab" data-toggle="tab" href="#characters" role="tab" aria-controls="profile" aria-selected="false">Characters</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="tabs-tabContent">
|
||||
<div class="tab-pane fade show active" id="search" role="tabpanel" aria-labelledby="search-tab">
|
||||
<form name="search" class="form-inline" on:submit|preventDefault={submitSearch}>
|
||||
<div class="form-group mx-sm-3 mb-2">
|
||||
<label for="inputSearch" class="sr-only">Search</label>
|
||||
<input type="text" class="form-control" id="inputSearch" name="search" placeholder="Search" minlength=3 required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2">Search</button>
|
||||
</form>
|
||||
{#if results}
|
||||
<ol>
|
||||
{#each results as result, i}
|
||||
{#if result.type == "account"}
|
||||
<li><AccountLink account={result} /></li>
|
||||
{:else if result.type == "character"}
|
||||
<li><CharacterLink character={result} /></li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="users" role="tabpanel" aria-labelledby="users-tab">
|
||||
<UserList {appAlert} />
|
||||
</div>
|
||||
<div class="tab-pane fade" id="characters" role="tabpanel" aria-labelledby="characters-tab">
|
||||
<CharacterList {appAlert} />
|
||||
</div>
|
||||
</div>
|
||||
17
app/views/BadRoute.svelte
Normal file
17
app/views/BadRoute.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import axios from 'axios'
|
||||
export let params;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await axios.post("/api/bad_route", {"route": params[0]})
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<img alt="404" src="/img/404.png" />
|
||||
<h1>404 - PlanetSide Not Found</h1>
|
||||
</div>
|
||||
45
app/views/CharacterList.svelte
Normal file
45
app/views/CharacterList.svelte
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import axios from 'axios'
|
||||
import CharacterLink from '../components/CharacterLink'
|
||||
import PaginatedList from '../components/PaginatedList'
|
||||
import moment from 'moment'
|
||||
|
||||
export let appAlert
|
||||
|
||||
async function fetch(page) {
|
||||
try {
|
||||
const resp = await axios.get("/api/characters?page="+page)
|
||||
appAlert.message("")
|
||||
return [resp.data.characters, resp.data.page];
|
||||
} catch (e) {
|
||||
appAlert.message(e.message)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<PaginatedList bind:fetch={fetch} let:data={characters} let:pagination={pagination}>
|
||||
<div slot="header">
|
||||
<p>{pagination.item_count.toLocaleString()} characters in the database</p>
|
||||
</div>
|
||||
|
||||
<table slot="body" class="table table-dark table-responsive">
|
||||
<thead>
|
||||
<td>ID</td>
|
||||
<td>Name</td>
|
||||
<td>Last Played</td>
|
||||
<td>Created</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each characters as char, i}
|
||||
<tr>
|
||||
<td>#{char.id}</td>
|
||||
<td><CharacterLink character={char} /></td>
|
||||
<td>{moment(char.last_login).fromNow()}</td>
|
||||
<td>{moment(char.created).fromNow()}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</PaginatedList>
|
||||
66
app/views/Home.svelte
Normal file
66
app/views/Home.svelte
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
|
||||
import { loggedIn } from '../UserState'
|
||||
import Alert from '../components/Alert'
|
||||
import AccountLink from '../components/AccountLink'
|
||||
import CharacterLink from '../components/CharacterLink'
|
||||
import EmpireStats from '../components/EmpireStats'
|
||||
|
||||
export let ready;
|
||||
|
||||
let stats;
|
||||
let alert;
|
||||
|
||||
function format_account(account) {
|
||||
return `<a href="/user/${account.id}">${account.username}</a>`
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const resp = await axios.get("/api/stats")
|
||||
stats = resp.data;
|
||||
alert.message("")
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
alert.message("Failed to fetch stats from server")
|
||||
}
|
||||
|
||||
ready = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>PSForever</title>
|
||||
</svelte:head>
|
||||
|
||||
<Alert bind:this={alert} />
|
||||
|
||||
{#if stats}
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<h1>PSForever Beta Server</h1>
|
||||
<p>
|
||||
<strong>Server address:</strong> <code>play.psforever.net:51200</code> (<a href="https://docs.google.com/document/d/1ZMx1NUylVZCXJNRyhkuVWT0eUKSVYu0JXsU-y3f93BY/edit">Setup Instructions</a>)<br/>
|
||||
<strong>PSForever accounts:</strong> {stats.accounts.toLocaleString()}<br/>
|
||||
<strong>Server characters:</strong> {stats.characters.toLocaleString()}<br/>
|
||||
<strong>Last character created:</strong> <CharacterLink character={stats.last.character} /> (<span title={moment(stats.last.character.created).format(`MMMM Do YYYY, h:mm:ss a`)}>{moment(stats.last.character.created).fromNow()}</span>)</p>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<EmpireStats stats={stats.empires} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{#if !$loggedIn}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-primary" href="/login" role="button">Login</a>
|
||||
<a class="btn btn-primary" href="/register" role="button">Create Account</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
91
app/views/Login.svelte
Normal file
91
app/views/Login.svelte
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import axios from 'axios'
|
||||
import page from 'page';
|
||||
import Alert from '../components/Alert'
|
||||
import { fade } from 'svelte/transition';
|
||||
import { formToJSON } from '../util/form.js'
|
||||
import { get_initial_state } from '../UserState.js';
|
||||
|
||||
export let ready = false;
|
||||
|
||||
const redirect = (new URL(window.location.href)).searchParams.get('redirect')
|
||||
let loginAttempts = 0;
|
||||
let alert;
|
||||
|
||||
onMount(async () => {
|
||||
// always check the initial state to see if we need to display this or not
|
||||
if (await get_initial_state()) {
|
||||
if (redirect)
|
||||
page.redirect(redirect)
|
||||
else
|
||||
page.redirect("/")
|
||||
|
||||
} else {
|
||||
ready = true;
|
||||
}
|
||||
});
|
||||
|
||||
async function submitLogin(e) {
|
||||
try {
|
||||
const resp = await axios.post("/api/login", formToJSON(event.target))
|
||||
|
||||
if (await get_initial_state()) {
|
||||
if (redirect)
|
||||
page.redirect(redirect)
|
||||
else
|
||||
page.redirect("/")
|
||||
|
||||
alert.message()
|
||||
loginAttempts = 0;
|
||||
} else {
|
||||
alert.message("Unknown login failure")
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
if (e.response.status >= 500) {
|
||||
alert.message("Unknown server error. Contact an administrator if this persists.")
|
||||
} else if (e.response.status === 403) {
|
||||
loginAttempts++;
|
||||
const badpass = "Bad username and/or password."
|
||||
|
||||
if (loginAttempts >= 5) {
|
||||
alert.message(badpass, `If you cannot remember your credentials, <a href="/recovery">reset them</a>. Otherwise you risk being locked out.`)
|
||||
} else {
|
||||
alert.message(badpass)
|
||||
}
|
||||
} else {
|
||||
alert.message("Unknown server error status");
|
||||
}
|
||||
} else if (e.request) {
|
||||
alert.message("Unknown server error. Contact an administrator if this persists.")
|
||||
} else {
|
||||
alert.message("Unknown request error: " + e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>PSForever - Login</title>
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<h1>Login to PSForever</h1>
|
||||
|
||||
<Alert bind:this={alert} />
|
||||
|
||||
<form name="login" class="form-group" on:submit|preventDefault={submitLogin}>
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input class="form-control" id="inputUsername" placeholder="Username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input class="form-control" type="password" id="inputPassword" placeholder="Password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p><a href="/recovery">Forgot username/password?</a></p>
|
||||
</main>
|
||||
82
app/views/Profile.svelte
Normal file
82
app/views/Profile.svelte
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import axios from 'axios'
|
||||
import page from 'page'
|
||||
import moment from 'moment'
|
||||
import CharacterLink from '../components/CharacterLink'
|
||||
import LoginList from '../components/LoginList'
|
||||
|
||||
export let ready;
|
||||
ready = false;
|
||||
|
||||
let username;
|
||||
let characters = [];
|
||||
let createDate;
|
||||
let isAdmin;
|
||||
let email;
|
||||
let account;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const resp = await axios.get("/api/user/profile")
|
||||
account = resp.data;
|
||||
username = resp.data.name;
|
||||
characters = resp.data.characters;
|
||||
createDate = moment(resp.data.account_created).format('MMMM Do YYYY, h:mm:ss a')
|
||||
+ " (" + moment(resp.data.account_created).fromNow() + ")";
|
||||
isAdmin = resp.data.admin;
|
||||
email = resp.data.email;
|
||||
|
||||
ready = true
|
||||
} catch (e) {
|
||||
if (e.response && e.response.status == 403) {
|
||||
page("/login?redirect=/profile")
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>PSForever - Profile</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Your Account</h1>
|
||||
<form>
|
||||
{#if isAdmin}
|
||||
<strong class="color-red">You are a GM.</strong>
|
||||
{/if}
|
||||
<div class="form-group row">
|
||||
<label for="staticUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="staticUsername" bind:value={username}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="staticEmail" class="col-sm-2 col-form-label">Email</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="staticEmail" bind:value={email}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="staticAccountCreated" class="col-sm-2 col-form-label">Account Created</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" id="staticAccountCreated" bind:value={createDate}>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2>Characters</h2>
|
||||
{#if characters.length > 1}
|
||||
<div class="row">
|
||||
{#each characters as char, i}
|
||||
<div class="col-md-4 col-12"><CharacterLink character={char} /></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p>You have no characters</p>
|
||||
{/if}
|
||||
|
||||
<h2>Logins</h2>
|
||||
{#if account}
|
||||
<LoginList account_id={account.id} />
|
||||
{/if}
|
||||
109
app/views/Register.svelte
Normal file
109
app/views/Register.svelte
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import page from 'page';
|
||||
import Alert from '../components/Alert'
|
||||
import { formToJSON } from '../util/form.js'
|
||||
import { get_initial_state } from '../UserState.js';
|
||||
|
||||
let alert;
|
||||
let validated = false;
|
||||
let email, password; // for validation
|
||||
|
||||
function validatePassword(e) {
|
||||
// cant use cpassword here because svelte has not propagated the bound value yet
|
||||
if (password !== e.target.value)
|
||||
e.target.setCustomValidity("Passwords do not match")
|
||||
else
|
||||
e.target.setCustomValidity("")
|
||||
}
|
||||
|
||||
function validateEmail(e) {
|
||||
if (email !== e.target.value)
|
||||
e.target.setCustomValidity("Emails do not match")
|
||||
else
|
||||
e.target.setCustomValidity("")
|
||||
}
|
||||
|
||||
async function submitLogin(e) {
|
||||
const data = formToJSON(e.target);
|
||||
|
||||
if (e.target.checkValidity() === false) {
|
||||
validated = true
|
||||
return
|
||||
}
|
||||
|
||||
validated = true
|
||||
|
||||
try {
|
||||
delete data.cpassword
|
||||
delete data.cemail
|
||||
const resp = await axios.post("/api/register", data);
|
||||
|
||||
if (await get_initial_state()) {
|
||||
page.redirect("/")
|
||||
} else {
|
||||
alert.message("Unknown login failure")
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
if (e.response.status >= 500) {
|
||||
alert.message("Unknown server error. Contact an administrator if this persists.")
|
||||
} else if (e.response.status === 400) {
|
||||
alert.message(e.response.data.message)
|
||||
} else {
|
||||
alert.message("Unknown server error status");
|
||||
}
|
||||
} else if (e.request) {
|
||||
alert.message("Unknown server error. Contact an administrator if this persists.")
|
||||
} else {
|
||||
alert.message("Unknown request error: " + e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>PSForever - Register</title>
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<h1>Register for PSForever</h1>
|
||||
|
||||
<Alert bind:this={alert} />
|
||||
|
||||
<form name="login" class:was-validated={validated} class="form-group needs-validation" novalidate on:submit|preventDefault={submitLogin}>
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input class="form-control" id="inputUsername" placeholder="Username" name="username" minlength=3 pattern={String.raw`[A-Za-z0-9]{3,}`} required>
|
||||
<small id="emailHelp" class="form-text text-muted">This is used to login via the game client, launcher, and web interface.</small>
|
||||
<div class="invalid-feedback">
|
||||
Usernames must be at least 3 characters long and not contain special characters or spaces.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input bind:value={password} class="form-control" type="password" id="inputPassword" placeholder="Password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<label for="inputCPassword">Confim Password</label>
|
||||
<input on:input={validatePassword} on:change={validatePassword} class="form-control" type="password" id="inputCPassword" placeholder="Confirm Password" name="cpassword" required>
|
||||
<div class="invalid-feedback">Passwords must match.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col">
|
||||
<label for="inputEmail">Email</label>
|
||||
<input bind:value={email} class="form-control" type="email" id="inputEmail" placeholder="Email" name="email" required>
|
||||
<small id="emailHelp" class="form-text text-muted">Emails are used to help confirm and recover accounts.</small>
|
||||
<div class="invalid-feedback">Please provide a valid email address.</div>
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<label for="inputCEmail">Confirm Email</label>
|
||||
<input on:input={validateEmail} on:change={validateEmail} class="form-control" type="email" id="inputCEmail" placeholder="Confirm Email" name="cemail" required>
|
||||
<div class="invalid-feedback">Email addresses must match.</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Join the fight!</button>
|
||||
</form>
|
||||
</main>
|
||||
4
app/views/User.svelte
Normal file
4
app/views/User.svelte
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<script>
|
||||
</script>
|
||||
|
||||
|
||||
150
app/views/UserList.svelte
Normal file
150
app/views/UserList.svelte
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import axios from 'axios'
|
||||
import AccountLink from '../components/AccountLink'
|
||||
import PaginatedList from '../components/PaginatedList'
|
||||
import Alert from '../components/Alert'
|
||||
import moment from 'moment'
|
||||
import jq from 'jquery'
|
||||
|
||||
export let appAlert
|
||||
let modalAlert, userList;
|
||||
|
||||
onMount(() => setup_actions());
|
||||
|
||||
async function fetch(page) {
|
||||
try {
|
||||
const resp = await axios.get("/api/users?page="+page)
|
||||
appAlert.message("")
|
||||
return [resp.data.users, resp.data.page];
|
||||
} catch (e) {
|
||||
appAlert.message(e.message)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function setup_actions() {
|
||||
const modal = jq('#actionModal');
|
||||
|
||||
modal.on('hide.bs.modal', () => modalAlert.message(""));
|
||||
modal.on('show.bs.modal', (event) => {
|
||||
const button = jq(event.relatedTarget) // Button that triggered the modal
|
||||
const username = button.data('account-name')
|
||||
const account_id = button.data('account-id')
|
||||
const action_type = button.data('action')
|
||||
const action_name = button.text();
|
||||
|
||||
modal.find('.modal-title').text("Confirm " + action_name)
|
||||
modal.find('.modal-body p').text("Are you sure you want to perform this action on \'" + username + "\'?")
|
||||
|
||||
const submit = modal.find('.modal-footer .btn-primary')
|
||||
|
||||
submit.text(action_name)
|
||||
// remove ALL previous click handlers
|
||||
submit.off()
|
||||
|
||||
submit.click(async (event) => {
|
||||
submit.addClass("disabled")
|
||||
|
||||
try {
|
||||
await axios.post("/api/user/"+ account_id + "/" + action_type)
|
||||
await userList.refresh()
|
||||
modal.modal('hide')
|
||||
} catch (e) {
|
||||
modalAlert.message(e.message)
|
||||
} finally {
|
||||
submit.removeClass("disabled")
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<PaginatedList bind:this={userList} bind:fetch={fetch} let:data={users} let:pagination={pagination}>
|
||||
<div slot="header">
|
||||
<p>{pagination.item_count.toLocaleString()} users in the database</p>
|
||||
</div>
|
||||
|
||||
<table slot="body" class="table table-dark table-responsive">
|
||||
<thead>
|
||||
<td>ID</td>
|
||||
<td>Username</td>
|
||||
<td>User Created</td>
|
||||
<td>Last Login</td>
|
||||
<td>Actions</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user, i}
|
||||
<tr>
|
||||
<td>#{user.id}</td>
|
||||
<td><AccountLink account={user} /></td>
|
||||
<td>{moment(user.created).fromNow()}</td>
|
||||
<td>{#if user.last_login.time}
|
||||
{moment(user.last_login.time).fromNow()}<br/>
|
||||
<code>{user.last_login.hostname} - {user.last_login.ip}</code>
|
||||
{:else}
|
||||
Never logged in
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if user.inactive}
|
||||
<button type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
data-action="unban"
|
||||
data-account-id={user.id}
|
||||
data-account-name={user.name}
|
||||
data-toggle="modal"
|
||||
data-target="#actionModal">Unban</button>
|
||||
{:else}
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm"
|
||||
data-action="ban"
|
||||
data-account-id={user.id}
|
||||
data-account-name={user.name}
|
||||
data-toggle="modal"
|
||||
data-target="#actionModal">Ban</button>
|
||||
{#if user.admin}
|
||||
<button type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
data-action="remove_gm"
|
||||
data-account-id={user.id}
|
||||
data-account-name={user.name}
|
||||
data-toggle="modal"
|
||||
data-target="#actionModal">Remove GM</button>
|
||||
{:else}
|
||||
<button type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
data-action="add_gm"
|
||||
data-account-id={user.id}
|
||||
data-account-name={user.name}
|
||||
data-toggle="modal"
|
||||
data-target="#actionModal">Make GM</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</PaginatedList>
|
||||
|
||||
<div class="modal fade" id="actionModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Perform Action</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<Alert bind:this={modalAlert} />
|
||||
<p>Are you sure?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">No</button>
|
||||
<button type="button" class="btn btn-primary">Yes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue