Overhaul design

This commit is contained in:
rxdn 2024-08-30 00:09:42 +01:00
parent 3fd7539d7f
commit 5eb26c3b53
20 changed files with 627 additions and 407 deletions

View File

@ -0,0 +1,29 @@
package api
import (
"github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
)
func GuildHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
guild, err := botContext.GetGuild(ctx, guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, gin.H{
"id": guild.Id,
"name": guild.Name,
"icon": guild.Icon,
})
}

View File

@ -94,6 +94,7 @@ func StartServer(sm *livechat.SocketManager) {
guildAuthApiSupport := apiGroup.Group("/:id", middleware.AuthenticateGuild(permission.Support)) guildAuthApiSupport := apiGroup.Group("/:id", middleware.AuthenticateGuild(permission.Support))
guildApiNoAuth := apiGroup.Group("/:id", middleware.ParseGuildId) guildApiNoAuth := apiGroup.Group("/:id", middleware.ParseGuildId)
{ {
guildAuthApiSupport.GET("/guild", api.GuildHandler)
guildAuthApiSupport.GET("/channels", api.ChannelsHandler) guildAuthApiSupport.GET("/channels", api.ChannelsHandler)
guildAuthApiSupport.GET("/premium", api.PremiumHandler) guildAuthApiSupport.GET("/premium", api.PremiumHandler)
guildAuthApiSupport.GET("/user/:user", api.UserHandler) guildAuthApiSupport.GET("/user/:user", api.UserHandler)

View File

@ -1,4 +1,10 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
:root {
--primary: #995DF3;
--primary-gradient: linear-gradient(71.3deg, #873ef5 0%, #995DF3 100%);
--blue: #3472f7;
}
html, body { html, body {
position: relative; position: relative;
@ -7,7 +13,7 @@ html, body {
} }
body, h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6, p, .navbar, .brand, .btn-simple, .alert, a, .td-name, td, button.close { body, h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6, p, .navbar, .brand, .btn-simple, .alert, a, .td-name, td, button.close {
font-family: 'Noto Sans', sans-serif !important; font-family: 'Poppins', sans-serif !important;
font-weight: 400 !important; font-weight: 400 !important;
} }

View File

@ -3,7 +3,7 @@
</div> </div>
<script> <script>
export let colour = '#3472f7'; export let colour = '';
</script> </script>
<style> <style>
@ -11,7 +11,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
background-color: var(--badge-background-color, #3472f7); background: var(--badge-background-color, var(--primary));
border-radius: 2px; border-radius: 2px;
font-size: 14px; font-size: 14px;
padding: 0 4px; padding: 0 4px;

View File

@ -28,9 +28,10 @@
text-align: center; text-align: center;
color: white; color: white;
background-color: #3472f7; background: var(--primary-gradient);
border-color: #3472f7; border: none;
border-width: 2px; /*border-color: var(--primary);*/
/*border-width: 2px;*/
border-radius: .25rem; border-radius: .25rem;
margin: 0; margin: 0;
@ -39,14 +40,14 @@
box-shadow: 0 4px 4px rgb(0 0 0 / 25%); box-shadow: 0 4px 4px rgb(0 0 0 / 25%);
} }
button:active, button:hover:enabled { /*button:active, button:hover:enabled {*/
background-color: #0062cc; /* background-color: #0062cc;*/
border-color: #0062cc; /* border-color: #0062cc;*/
} /*}*/
button:disabled { button:disabled {
background-color: #6c757d; background: #6c757d;
border-color: #6c757d; border: #6c757d;
cursor: default; cursor: default;
} }
@ -60,12 +61,12 @@
} }
.danger { .danger {
background-color: #dc3545 !important; background: #dc3545 !important;
border-color: #dc3545 !important; border-color: #dc3545 !important;
} }
.danger:hover:enabled, .danger:active { .danger:hover:enabled, .danger:active {
background-color: #c32232 !important; background: #c32232 !important;
border-color: #c32232 !important; border-color: #c32232 !important;
} }

View File

@ -0,0 +1,136 @@
<section class="sidebar">
<header>
<img src="{getIconUrl()}" class="guild-icon" alt="Guild icon" width="50" height="50"/>
{guild.name}
</header>
<nav>
<ul class="nav-list">
<ManageSidebarLink {currentRoute} title="← Back to servers" href="/" />
{#if isAdmin}
<ManageSidebarLink {currentRoute} title="Settings" icon="fa-cogs" href="/manage/{guildId}/settings" />
{/if}
<ManageSidebarLink {currentRoute} title="Transcripts" icon="fa-copy" href="/manage/{guildId}/transcripts" />
{#if isAdmin}
<ManageSidebarLink {currentRoute} routePrefix="/manage/{guildId}/panels" title="Ticket Panels" icon="fa-mouse-pointer" href="/manage/{guildId}/panels" />
<ManageSidebarLink {currentRoute} title="Forms" icon="fa-poll-h" href="/manage/{guildId}/forms" />
<ManageSidebarLink {currentRoute} title="Staff Teams" icon="fa-users" href="/manage/{guildId}/teams" />
<ManageSidebarLink {currentRoute} title="Integrations" icon="fa-robot" href="/manage/{guildId}/integrations" />
{/if}
<ManageSidebarLink {currentRoute} title="Tickets" icon="fa-ticket-alt" href="/manage/{guildId}/tickets" />
<ManageSidebarLink {currentRoute} title="Blacklist" icon="fa-ban" href="/manage/{guildId}/blacklist" />
<ManageSidebarLink {currentRoute} title="Tags" icon="fa-tags" href="/manage/{guildId}/tags" />
</ul>
</nav>
<nav class="bottom">
<hr/>
<ul class="nav-list">
<ManageSidebarLink {currentRoute} title="Documentation" icon="fa-book" href="https://docs.ticketsbot.net" newWindow />
<ManageSidebarLink {currentRoute} title="Logout" icon="fa-sign-out-alt" href="/logout" />
</ul>
</nav>
</section>
<style>
.sidebar {
display: flex;
flex-direction: column;
align-self: flex-start;
background-color: #272727;
padding: 15px;
width: 275px;
border-radius: 6px;
user-select: none;
}
header {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-weight: bold;
padding: 6px 10px;
border-radius: 4px;
background: linear-gradient(33.3deg, #873ef5 0%, #995DF3 100%);
box-shadow: 0 6px 6px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1);
}
.guild-icon {
width: 48px;
height: 48px;
border-radius: 50%;
}
nav > ul {
list-style-type: none;
padding: 0;
margin: 0;
}
nav hr {
width: 40%;
padding-left: 20px;
}
</style>
<script>
import {onMount} from "svelte";
import axios from "axios";
import {API_URL} from "../js/constants";
import {notifyError, withLoadingScreen} from "../js/util";
import ManageSidebarLink from "./ManageSidebarLink.svelte";
import SubNavigation from "./SubNavigation.svelte";
import SubNavigationLink from "./SubNavigationLink.svelte";
export let currentRoute;
export let permissionLevel;
$: isAdmin = permissionLevel >= 2;
let guildId = currentRoute.namedParams.id;
let guild = {};
async function loadGuild() {
const res = await axios.get(`${API_URL}/api/${guildId}/guild`);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
guild = res.data;
}
function isAnimated() {
if (guild.icon === undefined || guild.icon === "") {
return false;
} else {
return guild.icon.startsWith('a_')
}
}
function getIconUrl() {
if (!guild.icon) {
return `https://cdn.discordapp.com/embed/avatars/${Number((BigInt(guildId) >> BigInt(22)) % BigInt(6))}.png`
}
if (isAnimated()) {
return `https:\/\/cdn.discordapp.com/icons/${guild.id}/${guild.icon}.gif?size=256`
} else {
return `https:\/\/cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=256`
}
}
onMount(async () => {
await withLoadingScreen(async () => {
await loadGuild();
})
});
</script>

View File

@ -0,0 +1,52 @@
<li>
<div style="width: 100%">
<a href="{href}" class:active target="{newWindow === true ? '_blank' : '_self'}">
{#if icon}
<i class="fas {icon}"/>
{/if}
<span>{title}</span>
</a>
{#if active}
<slot/>
{/if}
</div>
</li>
<style>
a.active {
background: linear-gradient(71.3deg, #873ef5 0%, #995DF3 100%);
box-shadow: 0 6px 6px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1);
}
a, a:link, a:hover, a:visited, a:active {
display: block;
color: inherit;
text-decoration: none;
font-size: 16px;
padding: 5px 10px 5px 20px;
border-radius: 4px;
}
i {
width: 20px;
text-align: center;
}
</style>
<script>
export let currentRoute;
export let title;
export let icon;
export let href = "#";
export let routePrefix;
export let newWindow;
let active = href !== "/" && ((routePrefix || href)?.toLowerCase() === currentRoute.name.toLowerCase() ||
currentRoute.name.toLowerCase().startsWith((routePrefix || href).toLowerCase()));
$: active;
</script>

View File

@ -19,12 +19,7 @@
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Ticket Panels</NavElement> <NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Ticket Panels</NavElement>
<NavElement icon="fas fa-poll-h" link="/manage/{guildId}/forms" on:click={closeDropdown}>Forms</NavElement> <NavElement icon="fas fa-poll-h" link="/manage/{guildId}/forms" on:click={closeDropdown}>Forms</NavElement>
<NavElement icon="fas fa-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Staff Teams</NavElement> <NavElement icon="fas fa-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Staff Teams</NavElement>
<NavElement icon="fas fa-robot" link="/manage/{guildId}/integrations" on:click={closeDropdown}> <NavElement icon="fas fa-robot" link="/manage/{guildId}/integrations" on:click={closeDropdown}>Integrations</NavElement>
<div style="display: flex; gap:4px">
Integrations
<Badge>New!</Badge>
</div>
</NavElement>
{/if} {/if}
<NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement> <NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement>
@ -63,7 +58,7 @@
<style> <style>
.navbar { .navbar {
display: flex; display: none;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
background-color: #272727; background-color: #272727;
@ -89,6 +84,10 @@
} }
@media only screen and (max-width: 1154px) { @media only screen and (max-width: 1154px) {
.navbar {
display: flex;
}
.nav-section { .nav-section {
display: none; display: none;
} }

View File

@ -0,0 +1,19 @@
<div class="root">
<ul>
<slot/>
</ul>
</div>
<style>
.root {
display: flex;
flex-direction: row;
}
ul {
width: 100%;
list-style-type: none;
margin: 2px 0 0 55px;
padding: 0;
}
</style>

View File

@ -0,0 +1,53 @@
<li>
<div class="wrapper">
<a href="{href}" class:active>
{#if icon}
<i class="fas {icon}"/>
{/if}
<span>
<slot/>
</span>
</a>
</div>
</li>
<style>
li {
/*padding: 2px 0;*/
}
.wrapper {
width: 100%;
}
a.active {
font-weight: 600 !important;
}
a:not(.active) {
opacity: 0.75;
}
a, a:link, a:hover, a:visited, a:active {
color: inherit;
text-decoration: none;
}
i {
width: 20px;
text-align: center;
}
</style>
<script>
export let currentRoute;
export let icon;
export let href = "#";
export let routePrefix;
let active = href !== "/" && ((routePrefix || href).toLowerCase() === currentRoute.name.toLowerCase() ||
currentRoute.name.toLowerCase().startsWith((routePrefix || href).toLowerCase()));
$: active;
</script>

View File

@ -5,6 +5,7 @@
<div class="super-container" class:dropdown={$dropdown}> <div class="super-container" class:dropdown={$dropdown}>
<LoadingScreen/> <LoadingScreen/>
<div class="content-container" class:hide={$loadingScreen}> <div class="content-container" class:hide={$loadingScreen}>
<ManageSidebar {currentRoute} {permissionLevel} />
<Route {currentRoute} {params}/> <Route {currentRoute} {params}/>
</div> </div>
<NotifyModal/> <NotifyModal/>
@ -23,8 +24,13 @@
height: 100%; height: 100%;
} }
.super-container {
padding: 30px;
}
.content-container { .content-container {
display: flex; display: flex;
gap: 30px;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@ -48,6 +54,7 @@
import {setDefaultHeaders} from '../includes/Auth.svelte' import {setDefaultHeaders} from '../includes/Auth.svelte'
import {permissionLevelCache} from '../js/stores'; import {permissionLevelCache} from '../js/stores';
import {get} from 'svelte/store'; import {get} from 'svelte/store';
import ManageSidebar from "../includes/ManageSidebar.svelte";
export let currentRoute; export let currentRoute;
export let params = {}; export let params = {};

View File

@ -1,141 +1,141 @@
{#if blacklistUserModal} {#if blacklistUserModal}
<div class="modal" transition:fade> <div class="modal" transition:fade>
<div class="modal-wrapper"> <div class="modal-wrapper">
<Card footer footerRight fill={false}> <Card footer footerRight fill={false}>
<span slot="title">Blacklist User</span> <span slot="title">Blacklist User</span>
<div slot="body" class="modal-inner"> <div slot="body" class="modal-inner">
<div> <div>
<label class="form-label" style="margin-bottom: 0 !important;">Use User ID</label> <label class="form-label" style="margin-bottom: 0 !important;">Use User ID</label>
<Toggle hideLabel <Toggle hideLabel
toggledColor="#66bb6a" toggledColor="#66bb6a"
untoggledColor="#ccc" untoggledColor="#ccc"
bind:toggled={blacklistById}/> bind:toggled={blacklistById}/>
</div> </div>
{#if blacklistById} {#if blacklistById}
<Input label="User ID" placeholder="592348585904198711" bind:value={blacklistUserId}/> <Input label="User ID" placeholder="592348585904198711" bind:value={blacklistUserId}/>
{:else} {:else}
<div class="user-select-wrapper"> <div class="user-select-wrapper">
<UserSelect {guildId} label="User" bind:value={blacklistUser} /> <UserSelect {guildId} label="User" bind:value={blacklistUser}/>
</div> </div>
{/if} {/if}
</div>
<div slot="footer" style="gap: 12px">
<Button danger on:click={() => blacklistUserModal = false}>Cancel</Button>
<Button on:click={addUser}>Confirm</Button>
</div>
</Card>
</div> </div>
<div slot="footer" style="gap: 12px">
<Button danger on:click={() => blacklistUserModal = false}>Cancel</Button>
<Button on:click={addUser}>Confirm</Button>
</div>
</Card>
</div> </div>
</div>
<div class="modal-backdrop" transition:fade> <div class="modal-backdrop" transition:fade>
</div> </div>
{:else if blacklistRoleModal} {:else if blacklistRoleModal}
<div class="modal" transition:fade> <div class="modal" transition:fade>
<div class="modal-wrapper"> <div class="modal-wrapper">
<Card footer footerRight fill={false}> <Card footer footerRight fill={false}>
<span slot="title">Blacklist Role</span> <span slot="title">Blacklist Role</span>
<div slot="body" class="modal-inner user-select-wrapper"> <div slot="body" class="modal-inner user-select-wrapper">
<RoleSelect {guildId} {roles} label="Role" bind:value={blacklistRole} /> <RoleSelect {guildId} {roles} label="Role" bind:value={blacklistRole}/>
</div> </div>
<div slot="footer" style="gap: 12px"> <div slot="footer" style="gap: 12px">
<Button danger on:click={() => blacklistRoleModal = false}>Cancel</Button> <Button danger on:click={() => blacklistRoleModal = false}>Cancel</Button>
<Button on:click={addRole}>Confirm</Button> <Button on:click={addRole}>Confirm</Button>
</div>
</Card>
</div> </div>
</Card>
</div> </div>
</div>
<div class="modal-backdrop" transition:fade> <div class="modal-backdrop" transition:fade>
</div> </div>
{/if} {/if}
{#if data} {#if data}
<div class="parent">
<div class="content"> <div class="content">
<div class="main-col"> <div class="main-col">
<Card footer={false}> <Card footer={false}>
<span slot="title">Blacklist</span> <span slot="title">Blacklist</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<div class="row" style="gap: 10px"> <div class="row" style="gap: 10px">
<Button icon="fas fa-ban" on:click={() => blacklistUserModal = true}>Blacklist New User</Button> <Button icon="fas fa-ban" on:click={() => blacklistUserModal = true}>Blacklist New User</Button>
<Button icon="fas fa-ban" on:click={() => blacklistRoleModal = true}>Blacklist New Role</Button> <Button icon="fas fa-ban" on:click={() => blacklistRoleModal = true}>Blacklist New Role</Button>
</div> </div>
<hr/> <hr/>
<div class="tables"> <div class="tables">
<table class="nice"> <table class="nice">
<thead> <thead>
<tr> <tr>
<th class="full-width">Role</th> <th class="full-width">Role</th>
<th>Remove</th> <th>Remove</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each data.roles as roleId} {#each data.roles as roleId}
{@const role = roles.find(role => role.id === roleId)} {@const role = roles.find(role => role.id === roleId)}
<tr> <tr>
{#if role === undefined} {#if role === undefined}
<td class="full-width">Unknown ({roleId})</td> <td class="full-width">Unknown ({roleId})</td>
{:else} {:else}
<td class="full-width">{role.name}</td> <td class="full-width">{role.name}</td>
{/if} {/if}
<td> <td>
<Button type="button" danger icon="fas fa-trash-can" on:click={() => removeRoleBlacklist(roleId, role)}> <Button type="button" danger icon="fas fa-trash-can"
Remove on:click={() => removeRoleBlacklist(roleId, role)}>
</Button> Remove
</td> </Button>
</tr> </td>
{/each} </tr>
</tbody> {/each}
</table> </tbody>
</table>
<table class="nice"> <table class="nice">
<thead> <thead>
<tr> <tr>
<th class="full-width">User</th> <th class="full-width">User</th>
<th>Remove</th> <th>Remove</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each data.users as user} {#each data.users as user}
<tr> <tr>
{#if user.username !== ''} {#if user.username !== ''}
<td class="full-width">{user.username} ({user.id})</td> <td class="full-width">{user.username} ({user.id})</td>
{:else} {:else}
<td class="full-width">Unknown ({user.id})</td> <td class="full-width">Unknown ({user.id})</td>
{/if} {/if}
<td> <td>
<Button type="button" danger icon="fas fa-trash-can" on:click={() => removeUserBlacklist(user)}> <Button type="button" danger icon="fas fa-trash-can"
Remove on:click={() => removeUserBlacklist(user)}>
</Button> Remove
</td> </Button>
</tr> </td>
{/each} </tr>
</tbody> {/each}
</table> </tbody>
</div> </table>
</div>
<div class="row nav"> <div class="row nav">
<i class="fas fa-chevron-left pagination-chevron" class:disabled-chevron={page <= 1} <i class="fas fa-chevron-left pagination-chevron" class:disabled-chevron={page <= 1}
on:click={loadPrevious}></i> on:click={loadPrevious}></i>
<span>Page {page}</span> <span>Page {page}</span>
<i class="fas fa-chevron-right pagination-chevron" <i class="fas fa-chevron-right pagination-chevron"
class:disabled-chevron={data.users.length < data.page_limit && data.roles.length < data.page_limit} class:disabled-chevron={data.users.length < data.page_limit && data.roles.length < data.page_limit}
on:click={loadNext}></i> on:click={loadNext}></i>
</div> </div>
</div> </div>
</Card> </Card>
</div> </div>
</div> </div>
</div>
{/if} {/if}
<script> <script>
@ -296,21 +296,10 @@
</script> </script>
<style> <style>
.parent {
display: flex;
justify-content: flex-start;
padding-left: 2%;
width: 100%;
height: 100%;
}
.content { .content {
display: flex; display: flex;
justify-content: space-between; width: 100%;
width: 60%;
height: 100%; height: 100%;
margin-top: 30px;
padding-bottom: 4%;
} }
.main-col { .main-col {
@ -359,6 +348,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
margin-top: 20px;
} }
.pagination-chevron { .pagination-chevron {

View File

@ -1,83 +1,84 @@
<div class="parent"> <div class="content">
<div class="content">
<Card footer footerRight> <Card footer footerRight>
<span slot="title">Forms</span> <span slot="title">Forms</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<div class="section"> <div class="section">
<h2 class="section-title">Create New Form</h2> <h2 class="section-title">Create New Form</h2>
<form on:submit|preventDefault={createForm}> <form on:submit|preventDefault={createForm}>
<div class="row" id="creation-row"> <div class="row" id="creation-row">
<Input placeholder="Form Title" col3={true} bind:value={newTitle}/> <Input placeholder="Form Title" col3={true} bind:value={newTitle}/>
<div id="create-button-wrapper"> <div id="create-button-wrapper">
<Button icon="fas fa-paper-plane" fullWidth={windowWidth <= 950}>Create</Button> <Button icon="fas fa-paper-plane" fullWidth={windowWidth <= 950}>Create</Button>
</div> </div>
</div>
</form>
</div> </div>
</form> <div class="section">
</div> <h2 class="section-title">Manage Forms</h2>
<div class="section">
<h2 class="section-title">Manage Forms</h2>
{#if editingTitle && activeFormId !== null} {#if editingTitle && activeFormId !== null}
<div class="row form-name-edit-wrapper"> <div class="row form-name-edit-wrapper">
<Input col4 label="Form Title" placeholder="Form Title" bind:value={renamedTitle}/> <Input col4 label="Form Title" placeholder="Form Title" bind:value={renamedTitle}/>
<div class="form-name-save-wrapper"> <div class="form-name-save-wrapper">
<Button icon="fas fa-floppy-disk" fullWidth={windowWidth <= 950} on:click={updateTitle}>Save</Button> <Button icon="fas fa-floppy-disk" fullWidth={windowWidth <= 950} on:click={updateTitle}>
</div> Save
</div> </Button>
{:else} </div>
<div class="row form-select-row"> </div>
<div class="multiselect-super"> {:else}
<Dropdown col1 bind:value={activeFormId}> <div class="row form-select-row">
<option value={null}>Select a form...</option> <div class="multiselect-super">
{#each forms as form} <Dropdown col1 bind:value={activeFormId}>
<option value="{form.form_id}">{form.title}</option> <option value={null}>Select a form...</option>
{/each} {#each forms as form}
</Dropdown> <option value="{form.form_id}">{form.title}</option>
</div> {/each}
</Dropdown>
</div>
{#if activeFormId !== null} {#if activeFormId !== null}
<Button on:click={() => editingTitle = true}>Rename Form</Button> <Button on:click={() => editingTitle = true}>Rename Form</Button>
<Button danger type="button" <Button danger type="button"
on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button> on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button>
{/if} {/if}
</div> </div>
{/if} {/if}
<div class="manage"> <div class="manage">
{#if activeFormId !== null} {#if activeFormId !== null}
{#each forms.find(form => form.form_id === activeFormId).inputs as input, i (input)} {#each forms.find(form => form.form_id === activeFormId).inputs as input, i (input)}
<div animate:flip="{{duration: 500}}"> <div animate:flip="{{duration: 500}}">
<FormInputRow data={input} formId={activeFormId} <FormInputRow data={input} formId={activeFormId}
withSaveButton={true} withDeleteButton={true} withDirectionButtons={true} withSaveButton={true} withDeleteButton={true} withDirectionButtons={true}
index={i} {formLength} index={i} {formLength}
on:delete={() => deleteInput(activeFormId, input)} on:delete={() => deleteInput(activeFormId, input)}
on:move={(e) => changePosition(activeFormId, input, e.detail.direction)}/> on:move={(e) => changePosition(activeFormId, input, e.detail.direction)}/>
</div>
{/each}
{/if}
{#if activeFormId !== null}
<div class="row"
style="justify-content: center; align-items: center; gap: 10px; margin-top: 10px">
<hr class="fill">
<div class="row add-input-container" class:add-input-disabled={formLength >= 5}>
<i class="fas fa-plus"></i>
<a on:click={addInput}>New Field</a>
</div>
<hr class="fill">
</div>
{/if}
</div> </div>
{/each} </div>
{/if}
{#if activeFormId !== null}
<div class="row" style="justify-content: center; align-items: center; gap: 10px; margin-top: 10px">
<hr class="fill">
<div class="row add-input-container" class:add-input-disabled={formLength >= 5}>
<i class="fas fa-plus"></i>
<a on:click={addInput}>New Field</a>
</div>
<hr class="fill">
</div>
{/if}
</div>
</div> </div>
</div>
<div slot="footer"> <div slot="footer">
<Button type="submit" icon="fas fa-floppy-disk" disabled={formLength === 0} on:click={saveInputs}> <Button type="submit" icon="fas fa-floppy-disk" disabled={formLength === 0} on:click={saveInputs}>
Save Save
</Button> </Button>
</div> </div>
</Card> </Card>
</div>
</div> </div>
<svelte:window bind:innerWidth={windowWidth}/> <svelte:window bind:innerWidth={windowWidth}/>
@ -282,20 +283,10 @@
</script> </script>
<style> <style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.content { .content {
display: flex; display: flex;
justify-content: space-between; width: 100%;
width: 96%;
height: 100%; height: 100%;
margin-top: 30px;
margin-bottom: 50px;
} }
.body-wrapper { .body-wrapper {
@ -383,7 +374,7 @@
cursor: pointer; cursor: pointer;
} }
.add-input-disabled > *{ .add-input-disabled > * {
cursor: default !important; cursor: default !important;
color: #777 !important; color: #777 !important;
} }

View File

@ -19,8 +19,6 @@
flex-direction: row; flex-direction: row;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 0 45px;
justify-content: space-between;
} }
.main-col { .main-col {
@ -28,7 +26,6 @@
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 100%; width: 100%;
margin-top: 30px;
} }
.right-col { .right-col {

View File

@ -1,41 +1,37 @@
{#if tagCreateModal} {#if tagCreateModal}
<TagEditor {isPremium} on:cancel={() => tagCreateModal = false} on:confirm={createTag}/> <TagEditor {isPremium} on:cancel={() => tagCreateModal = false} on:confirm={createTag}/>
{:else if tagEditModal} {:else if tagEditModal}
<TagEditor {isPremium} bind:data={editData} on:cancel={cancelEdit} on:confirm={editTag}/> <TagEditor {isPremium} bind:data={editData} on:cancel={cancelEdit} on:confirm={editTag}/>
{/if} {/if}
<div class="parent"> <div class="content">
<div class="content"> <Card footer footerRight>
<div class="main-col">
<Card footer footerRight>
<span slot="title">Tags</span> <span slot="title">Tags</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<table class="nice"> <table class="nice">
<thead> <thead>
<tr> <tr>
<th>Tag</th> <th>Tag</th>
<th style="text-align: right">Actions</th> <th style="text-align: right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each Object.entries(tags) as [id, tag]} {#each Object.entries(tags) as [id, tag]}
<tr> <tr>
<td>{id}</td> <td>{id}</td>
<td class="actions"> <td class="actions">
<Button type="button" on:click={() => openEditModal(id)}>Edit</Button> <Button type="button" on:click={() => openEditModal(id)}>Edit</Button>
<Button type="button" danger={true} on:click={() => deleteTag(id)}>Delete</Button> <Button type="button" danger={true} on:click={() => deleteTag(id)}>Delete</Button>
</td> </td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
<div slot="footer"> <div slot="footer">
<Button icon="fas fa-plus" on:click={openCreateModal}>Create Tag</Button> <Button icon="fas fa-plus" on:click={openCreateModal}>Create Tag</Button>
</div> </div>
</Card> </Card>
</div>
</div>
</div> </div>
<script> <script>
@ -45,7 +41,6 @@
import axios from "axios"; import axios from "axios";
import {API_URL} from "../js/constants"; import {API_URL} from "../js/constants";
import {setDefaultHeaders} from '../includes/Auth.svelte' import {setDefaultHeaders} from '../includes/Auth.svelte'
import {fade} from "svelte/transition";
import TagEditor from "../components/manage/TagEditor.svelte"; import TagEditor from "../components/manage/TagEditor.svelte";
export let currentRoute; export let currentRoute;
@ -61,7 +56,7 @@
function openCreateModal(id) { function openCreateModal(id) {
tagCreateModal = true; tagCreateModal = true;
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({top: 0, behavior: 'smooth'});
} }
function openEditModal(id) { function openEditModal(id) {
@ -69,7 +64,7 @@
editData = tags[id]; editData = tags[id];
tagEditModal = true; tagEditModal = true;
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({top: 0, behavior: 'smooth'});
} }
function cancelEdit() { function cancelEdit() {
@ -192,27 +187,10 @@
</script> </script>
<style> <style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.content { .content {
display: flex; display: flex;
justify-content: space-between; width: 100%;
width: 96%;
height: 100%; height: 100%;
margin-top: 30px;
}
.main-col {
display: flex;
flex-direction: column;
width: 64%;
height: 100%;
padding-bottom: 4%;
} }
.body-wrapper { .body-wrapper {

View File

@ -1,4 +1,3 @@
<div class="parent">
<div class="content"> <div class="content">
<Card footer={false}> <Card footer={false}>
<span slot="title">Support Teams</span> <span slot="title">Support Teams</span>
@ -76,7 +75,6 @@
</div> </div>
</Card> </Card>
</div> </div>
</div>
<script> <script>
import Card from "../components/Card.svelte"; import Card from "../components/Card.svelte";
@ -226,19 +224,10 @@
</script> </script>
<style> <style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.content { .content {
display: flex; display: flex;
justify-content: space-between; width: 100%;
width: 96%;
height: 100%; height: 100%;
margin-top: 30px;
} }
.body-wrapper { .body-wrapper {

View File

@ -1,44 +1,42 @@
<div class="parent"> <div class="content">
<div class="content">
<Card footer={false}> <Card footer={false}>
<span slot="title">Open Tickets</span> <span slot="title">Open Tickets</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<table class="nice"> <table class="nice">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Panel</th> <th>Panel</th>
<th>User</th> <th>User</th>
<th>View</th> <th>View</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each tickets as ticket} {#each tickets as ticket}
<tr> <tr>
<td>{ticket.id}</td> <td>{ticket.id}</td>
<td>{ticket.panel_title}</td> <td>{ticket.panel_title}</td>
{#if ticket.user !== undefined} {#if ticket.user !== undefined}
<td>{ticket.user.username}</td> <td>{ticket.user.username}</td>
{:else} {:else}
<td>Unknown</td> <td>Unknown</td>
{/if} {/if}
<td> <td>
<Navigate to="/manage/{guildId}/tickets/view/{ticket.id}" styles="link"> <Navigate to="/manage/{guildId}/tickets/view/{ticket.id}" styles="link">
<Button type="button">View</Button> <Button type="button">View</Button>
</Navigate> </Navigate>
</td> </td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
</Card> </Card>
</div>
</div> </div>
<script> <script>
import Card from "../components/Card.svelte"; import Card from "../components/Card.svelte";
import {notifyError, notifySuccess, withLoadingScreen} from '../js/util' import {notifyError, withLoadingScreen} from '../js/util'
import axios from "axios"; import axios from "axios";
import {API_URL} from "../js/constants"; import {API_URL} from "../js/constants";
import {setDefaultHeaders} from '../includes/Auth.svelte' import {setDefaultHeaders} from '../includes/Auth.svelte'
@ -67,20 +65,10 @@
</script> </script>
<style> <style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
margin-top: 30px;
}
.content { .content {
display: flex; display: flex;
justify-content: space-between; width: 100%;
width: 96%;
height: 100%; height: 100%;
margin-top: 30px;
} }
.body-wrapper { .body-wrapper {

View File

@ -242,9 +242,8 @@
.col { .col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 95%;
height: 100%; height: 100%;
margin-top: 30px; width: 100%;
} }
.main-col { .main-col {

View File

@ -1,65 +1,65 @@
<div class="parent"> <div class="content">
<div class="content">
<div class="container"> <div class="container">
<div class="spread"> <div class="spread">
<h4 class="title">My Integrations</h4> <h4 class="title">My Integrations</h4>
<Button icon="fas fa-server" on:click={() => navigateTo(`/manage/${guildId}/integrations/create`)}>Create <Button icon="fas fa-server" on:click={() => navigateTo(`/manage/${guildId}/integrations/create`)}>Create
Integration Integration
</Button> </Button>
</div> </div>
<div class="integrations my-integrations"> <div class="integrations my-integrations">
{#each ownedIntegrations as integration} {#each ownedIntegrations as integration}
<div class="integration"> <div class="integration">
<Integration owned name={integration.name} {guildId} integrationId={integration.id} <Integration owned name={integration.name} {guildId} integrationId={integration.id}
imageUrl={generateProxyUrl(integration)} guildCount={integration.guild_count}> imageUrl={generateProxyUrl(integration)} guildCount={integration.guild_count}>
<span slot="description"> <span slot="description">
{integration.description} {integration.description}
</span> </span>
</Integration> </Integration>
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
<div> <div>
<h4 class="title">Available Integrations</h4> <h4 class="title">Available Integrations</h4>
<div class="integrations"> <div class="integrations">
<!-- Built in --> <!-- Built in -->
{#if page === 1} {#if page === 1}
<div class="integration"> <div class="integration">
<Integration builtIn name="Bloxlink" <Integration builtIn name="Bloxlink"
imageUrl="https://dbl-static.b-cdn.net/9bbd1f9504ddefc89606b19b290e9a0f.png" imageUrl="https://dbl-static.b-cdn.net/9bbd1f9504ddefc89606b19b290e9a0f.png"
viewLink="https://docs.ticketsbot.net/dashboard/settings/placeholders#bloxlink"> viewLink="https://docs.ticketsbot.net/dashboard/settings/placeholders#bloxlink">
<span slot="description"> <span slot="description">
Our Bloxlink integration inserts the Roblox usernames, profile URLs and more of your users into Our Bloxlink integration inserts the Roblox usernames, profile URLs and more of your users into
ticket welcome messages automatically! This integration is automatically enabled in all servers, press the ticket welcome messages automatically! This integration is automatically enabled in all servers, press the
View button below to check out the full list of placeholders you can use! View button below to check out the full list of placeholders you can use!
</span> </span>
</Integration> </Integration>
</div> </div>
{/if} {/if}
{#each availableIntegrations as integration} {#each availableIntegrations as integration}
<div class="integration"> <div class="integration">
<Integration name={integration.name} {guildId} integrationId={integration.id} <Integration name={integration.name} {guildId} integrationId={integration.id}
imageUrl={generateProxyUrl(integration)} ownerId={integration.owner_id} imageUrl={generateProxyUrl(integration)} ownerId={integration.owner_id}
added={integration.added} guildCount={integration.guild_count} showAuthor added={integration.added} guildCount={integration.guild_count} showAuthor
author={integration.author} on:remove={() => removeIntegration(integration.id)}> author={integration.author} on:remove={() => removeIntegration(integration.id)}>
<span slot="description"> <span slot="description">
{integration.description} {integration.description}
</span> </span>
</Integration> </Integration>
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
<div class="pagination"> <div class="pagination">
<i class="fas fa-chevron-left pagination-chevron" class:disabled-chevron={page === 1} on:click={previousPage}></i> <i class="fas fa-chevron-left pagination-chevron" class:disabled-chevron={page === 1}
<p>Page {page}</p> on:click={previousPage}></i>
<i class="fas fa-chevron-right pagination-chevron" class:disabled-chevron={!hasNextPage} on:click={nextPage}></i> <p>Page {page}</p>
<i class="fas fa-chevron-right pagination-chevron" class:disabled-chevron={!hasNextPage}
on:click={nextPage}></i>
</div> </div>
</div>
</div> </div>
<script> <script>
@ -161,21 +161,12 @@
</script> </script>
<style> <style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
width: 96%; width: 100%;
height: 100%; height: 100%;
margin-top: 30px;
padding-bottom: 5vh;
row-gap: 4vh; row-gap: 4vh;
} }
@ -193,7 +184,7 @@
} }
.integration { .integration {
flex: 0 0 23.5%; flex: 0 0 32%;
} }
.my-integrations { .my-integrations {
@ -231,19 +222,13 @@
cursor: default !important; cursor: default !important;
} }
@media only screen and (max-width: 1180px) { @media only screen and (max-width: 1200px) {
.integration {
flex: 0 0 32%;
}
}
@media only screen and (max-width: 930px) {
.integration { .integration {
flex: 0 0 49%; flex: 0 0 49%;
} }
} }
@media only screen and (max-width: 576px) { @media only screen and (max-width: 850px) {
.integration { .integration {
flex: 0 0 100%; flex: 0 0 100%;
} }

View File

@ -207,7 +207,7 @@
flex-direction: row; flex-direction: row;
height: 100%; height: 100%;
width: 100%; width: 100%;
margin-top: 30px; gap: 2%;
} }
.col { .col {
@ -219,7 +219,7 @@
.row { .row {
display: flex; display: flex;
width: 96%; width: 100%;
margin-bottom: 2%; margin-bottom: 2%;
} }