Tag overhaul

This commit is contained in:
rxdn 2022-08-02 15:09:55 +01:00
parent 859de14f00
commit dae9189052
17 changed files with 469 additions and 275 deletions

View File

@ -1,21 +1,35 @@
package api
import (
"github.com/TicketsBot/GoPanel/database"
"fmt"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"regexp"
"strings"
)
type tag struct {
Id string `json:"id"`
Content string `json:"content"`
Id string `json:"id" validate:"required,min=1,max=16"`
UseGuildCommand bool `json:"use_guild_command"` // Not yet implemented
Content *string `json:"content" validate:"omitempty,min=1,max=4096"`
UseEmbed bool `json:"use_embed"`
Embed *types.CustomEmbed `json:"embed" validate:"omitempty,dive"`
}
var (
validate = validator.New()
slashCommandRegex = regexp.MustCompile(`^[-_a-zA-Z0-9]{1,32}$`)
)
func CreateTag(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
// Max of 200 tags
count, err := database.Client.Tag.GetTagCount(guildId)
count, err := dbclient.Client.Tag.GetTagCount(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
@ -32,27 +46,80 @@ func CreateTag(ctx *gin.Context) {
return
}
if !data.verifyIdLength() {
ctx.JSON(400, utils.ErrorStr("Tag ID must be 1 - 16 characters in length"))
if !data.UseEmbed {
data.Embed = nil
}
// TODO: Limit command amount
if err := validate.Struct(data); err != nil {
validationErrors, ok := err.(validator.ValidationErrors)
if !ok {
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
return
}
formatted := "Your input contained the following errors:"
for _, validationError := range validationErrors {
formatted += fmt.Sprintf("\n%s", validationError.Error())
}
formatted = strings.TrimSuffix(formatted, "\n")
ctx.JSON(400, utils.ErrorStr(formatted))
return
}
if !data.verifyContentLength() {
ctx.JSON(400, utils.ErrorStr("Tag content must be 1 - 2000 characters in length"))
if !data.verifyContent() {
ctx.JSON(400, utils.ErrorStr("You have not provided any content for the tag"))
return
}
if err := database.Client.Tag.Set(guildId, data.Id, data.Content); err != nil {
var embed *database.CustomEmbedWithFields
if data.Embed != nil {
customEmbed, fields := data.Embed.IntoDatabaseStruct()
embed = &database.CustomEmbedWithFields{
CustomEmbed: customEmbed,
Fields: fields,
}
}
wrapped := database.Tag{
Id: data.Id,
GuildId: guildId,
UseGuildCommand: data.UseGuildCommand,
Content: data.Content,
Embed: embed,
}
if err := dbclient.Client.Tag.Set(wrapped); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.Status(204)
}
func (t *tag) verifyId() bool {
if len(t.Id) == 0 || len(t.Id) > 16 || strings.Contains(t.Id, " ") {
return false
}
if t.UseGuildCommand {
return slashCommandRegex.MatchString(t.Id)
} else {
ctx.JSON(200, utils.SuccessResponse)
return true
}
}
func (t *tag) verifyIdLength() bool {
return len(t.Id) > 0 && len(t.Id) <= 16
}
func (t *tag) verifyContent() bool {
if t.Content != nil { // validator ensures that if this is not nil, > 0 length
return true
}
func (t *tag) verifyContentLength() bool {
return len(t.Content) > 0 && len(t.Content) <= 2000
if t.Embed != nil {
if t.Embed.Description != nil || len(t.Embed.Fields) > 0 || t.Embed.ImageUrl != nil || t.Embed.ThumbnailUrl != nil {
return true
}
}
return false
}

View File

@ -6,14 +6,14 @@ import (
"github.com/gin-gonic/gin"
)
type deleteBody struct {
TagId string `json:"tag_id"`
}
func DeleteTag(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
type Body struct {
TagId string `json:"tag_id"`
}
var body Body
var body deleteBody
if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
@ -26,7 +26,8 @@ func DeleteTag(ctx *gin.Context) {
if err := database.Client.Tag.Delete(guildId, body.TagId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
} else {
ctx.JSON(200, utils.SuccessResponse)
return
}
ctx.Status(204)
}

View File

@ -2,21 +2,35 @@ package api
import (
"github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/gin-gonic/gin"
)
// TODO: Make client take new structure
func TagsListHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
tags, err := database.Client.Tag.GetByGuild(guildId)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, tags)
wrapped := make(map[string]tag)
for id, data := range tags {
var embed *types.CustomEmbed
if data.Embed != nil {
embed = types.NewCustomEmbed(data.Embed.CustomEmbed, data.Embed.Fields)
}
wrapped[id] = tag{
Id: data.Id,
UseGuildCommand: data.UseGuildCommand,
Content: data.Content,
UseEmbed: data.Embed != nil,
Embed: embed,
}
}
ctx.JSON(200, wrapped)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild"
"github.com/rxdn/gdl/objects/guild/emoji"
"github.com/rxdn/gdl/objects/interaction"
"github.com/rxdn/gdl/objects/member"
"github.com/rxdn/gdl/objects/user"
"github.com/rxdn/gdl/rest"
@ -168,3 +169,11 @@ func (ctx BotContext) ListMembers(guildId uint64) (members []member.Member, err
return
}
func (ctx BotContext) CreateGuildCommand(guildId uint64, data rest.CreateCommandData) (interaction.ApplicationCommand, error) {
return rest.CreateGuildCommand(ctx.Token, ctx.RateLimiter, ctx.BotId, guildId, data)
}
func (ctx BotContext) DeleteGuildCommand(guildId, commandId uint64) error {
return rest.DeleteGuildCommand(ctx.Token, ctx.RateLimiter, ctx.BotId, guildId, commandId)
}

View File

@ -1,7 +1,9 @@
<div class="modal" transition:fade>
<div class="modal-wrapper">
<Card footer="{true}" footerRight="{true}" fill="{false}">
<span slot="title">Embed Builder</span>
<span slot="title">
<slot name="title">Embed Builder</slot>
</span>
<div slot="body" class="body-wrapper">
<slot name="body"></slot>
@ -51,6 +53,7 @@
display: flex;
width: 60%;
margin: 10% auto auto auto;
padding-bottom: 5%;
}
@media only screen and (max-width: 1280px) {

View File

@ -4,71 +4,7 @@
<span slot="title">Embed Builder</span>
<div slot="body" class="body-wrapper">
<form class="form-wrapper" on:submit|preventDefault>
<div class="row">
<Colour col3 label="Embed Colour" bind:value={data.colour}/>
<Input col3 label="Title" placeholder="Embed Title" bind:value={data.title}/>
<Input col3 label="Title URL (Optional)" placeholder="https://example.com" bind:value={data.url}/>
</div>
<div class="row">
<Textarea col1 label="Description" placeholder="Large text area, up to 4096 characters"
bind:value={data.description}/>
</div>
<Collapsible>
<span slot="header">Author</span>
<div slot="content" class="row">
<Input col3 label="Author Name" placeholder="Author Name" bind:value={data.author.name}/>
<Input col3 label="Author Icon URL (Optional)" placeholder="https://example.com/image.png"
tooltipText="Small icon displayed in the top left" bind:value={data.author.icon_url}/>
<Input col3 label="Author URL (Optional)" placeholder="https://example.com"
tooltipText="Hyperlink on the author's name" bind:value={data.author.url}/>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Images</span>
<div slot="content" class="row">
<Input col2 label="Large Image URL" placeholder="https://example.com/image.png"
bind:value={data.image_url}/>
<Input col2 label="Small Image URL" placeholder="https://example.com/image.png"
bind:value={data.thumbnail_url}/>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Footer</span>
<div slot="content" class="row">
<Input col3 label="Footer Text" placeholder="Footer Text" badge="Premium" bind:value={data.footer.text}/>
<Input col3 label="Footer Icon URL (Optional)" badge="Premium" placeholder="https://example.com/image.png"
bind:value={data.footer.icon_url}/>
<DateTimePicker col3 label="Footer Timestamp (Optional)" bind:value={data.timestamp}/>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Fields</span>
<div slot="content" class="col-1">
{#each data.fields as field, i}
<div class="row" style="justify-content: flex-start; gap: 10px">
<Input col2 label="Field Name" placeholder="Field Name" bind:value={field.name}/>
<Checkbox label="Inline" bind:value={field.inline}/>
<div style="margin-top: 18px; display: flex; align-self: center">
<Button danger icon="fas fa-trash-can" on:click={() => deleteField(i)}>Delete</Button>
</div>
</div>
<div class="row">
<Textarea col1 label="Field Value" placeholder="Large text area, up to 1024 characters"
bind:value={field.value}/>
</div>
{/each}
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
</div>
</Collapsible>
</form>
<EmbedForm bind:data />
</div>
<div slot="footer">
@ -97,32 +33,12 @@
import DateTimePicker from "./form/DateTimePicker.svelte";
import Collapsible from "./Collapsible.svelte";
import Checkbox from "./form/Checkbox.svelte";
import EmbedForm from "./EmbedForm.svelte";
export let guildId;
export let data;
if (data === undefined || data === null) {
if (!data) {
data = {};
}
data.fields = [];
data.colour = '#2ECC71';
data.author = {};
data.footer = {};
}
function addField() {
data.fields.push({name: '', value: '', inline: false});
data = data;
}
function deleteField(i) {
data.fields.splice(i, 1);
data = data;
}
const dispatch = createEventDispatcher();
function dispatchClose() {
@ -190,19 +106,4 @@
background-color: #000;
opacity: .5;
}
.form-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,116 @@
<form class="form-wrapper" on:submit|preventDefault>
<div class="row">
<Colour col3 label="Embed Colour" bind:value={data.colour}/>
<Input col3 label="Title" placeholder="Embed Title" bind:value={data.title}/>
<Input col3 label="Title URL (Optional)" placeholder="https://example.com" bind:value={data.url}/>
</div>
<div class="row">
<Textarea col1 label="Description" placeholder="Large text area, up to 4096 characters"
bind:value={data.description}/>
</div>
<Collapsible>
<span slot="header">Author</span>
<div slot="content" class="row">
<Input col3 label="Author Name" placeholder="Author Name" bind:value={data.author.name}/>
<Input col3 label="Author Icon URL (Optional)" placeholder="https://example.com/image.png"
tooltipText="Small icon displayed in the top left" bind:value={data.author.icon_url}/>
<Input col3 label="Author URL (Optional)" placeholder="https://example.com"
tooltipText="Hyperlink on the author's name" bind:value={data.author.url}/>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Images</span>
<div slot="content" class="row">
<Input col2 label="Large Image URL" placeholder="https://example.com/image.png"
bind:value={data.image_url}/>
<Input col2 label="Small Image URL" placeholder="https://example.com/image.png"
bind:value={data.thumbnail_url}/>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Footer</span>
<div slot="content" class="row">
<Input col3 label="Footer Text" placeholder="Footer Text" badge="Premium" bind:value={data.footer.text}/>
<Input col3 label="Footer Icon URL (Optional)" badge="Premium" placeholder="https://example.com/image.png"
bind:value={data.footer.icon_url}/>
<DateTimePicker col3 label="Footer Timestamp (Optional)" bind:value={data.timestamp}/>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Fields</span>
<div slot="content" class="col-1">
{#each data.fields as field, i}
<div class="row" style="justify-content: flex-start; gap: 10px">
<Input col2 label="Field Name" placeholder="Field Name" bind:value={field.name}/>
<Checkbox label="Inline" bind:value={field.inline}/>
<div style="margin-top: 18px; display: flex; align-self: center">
<Button danger icon="fas fa-trash-can" on:click={() => deleteField(i)}>Delete</Button>
</div>
</div>
<div class="row">
<Textarea col1 label="Field Value" placeholder="Large text area, up to 1024 characters"
bind:value={field.value}/>
</div>
{/each}
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
</div>
</Collapsible>
</form>
<style>
.form-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 10px;
width: 100%;
height: 100%;
}
</style>
<script>
import Textarea from "./form/Textarea.svelte";
import Colour from "./form/Colour.svelte";
import Input from "./form/Input.svelte";
import Collapsible from "./Collapsible.svelte";
import DateTimePicker from "./form/DateTimePicker.svelte";
import Checkbox from "./form/Checkbox.svelte";
import Button from "./Button.svelte";
export let data;
if (data === undefined || data === null) {
if (!data) {
data = {};
}
data.fields = [];
data.colour = '#2ECC71';
data.author = {};
data.footer = {};
}
function addField() {
data.fields.push({name: '', value: '', inline: false});
data = data;
}
function deleteField(i) {
data.fields.splice(i, 1);
data = data;
}
</script>

View File

@ -18,5 +18,6 @@
.form-checkbox {
height: 40px;
width: 40px;
margin: 0 !important;
}
</style>

View File

@ -1,12 +1,12 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
{#if label !== undefined}
<div class="label-wrapper">
<label for="input" class="form-label">{label}</label>
<div class="label-wrapper" class:no-margin={tooltipText !== undefined}>
<label for="input" class="form-label" style="margin-bottom: 0">{label}</label>
{#if badge !== undefined}
<Badge>{badge}</Badge>
{/if}
{#if tooltipText !== undefined}
<div style="margin-bottom: 5px">
<div>
<Tooltip tip={tooltipText} top color="#121212">
{#if tooltipLink !== undefined}
<a href={tooltipLink} target="_blank">
@ -53,6 +53,11 @@
flex-direction: row;
align-items: center;
gap: 5px;
margin-bottom: 5px;
}
.no-margin {
margin-bottom: 0 !important;
}
.tooltip-icon {

View File

@ -1,70 +0,0 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4} class="switch">
<label for="input" class="form-label">{label}</label>
<input id="input" type="checkbox" bind:checked={value} on:change={() => console.log('b')} />
<span class="slider" />
</div>
<script>
export let value;
export let label;
export let col1 = false;
export let col2 = false;
export let col3 = false;
export let col4 = false;
</script>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196f3;
}
input:checked + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
</style>

View File

@ -1,4 +1,4 @@
<div>
<div class:inline>
{#if label !== undefined}
<label class="form-label" style="margin-bottom: 0 !important;">{label}</label>
{/if}
@ -16,4 +16,17 @@
export let toggledColour = "#66bb6a";
export let untoggledColour = "#ccc";
export let inline = false;
</script>
<style>
div {
display: flex;
flex-direction: column;
}
.inline {
flex-direction: row !important;
gap: 4px;
}
</style>

View File

@ -0,0 +1,98 @@
{#if data}
<ConfirmationModal icon="fas fa-floppy-disk" on:confirm={() => dispatch("confirm", data)} on:cancel={() => dispatch("cancel", {})}>
<span slot="title">Tag Editor</span>
<div slot="body" class="body-wrapper">
<div class="row">
<Input col4 label="Tag ID" placeholder="docs" bind:value={data.id}
tooltipText='If the command is "/tag docs", then the ID is "docs"'/>
</div>
<div class="row">
<Textarea col1 label="Message Content" bind:value={data.content} placeholder="Message content, outside of the embed"/>
</div>
<div class="row">
</div>
<div class="col">
<div class="inline">
<Toggle inline label="Use Embed" bind:value={data.use_embed}/>
<hr/>
</div>
{#if data.use_embed}
<EmbedForm bind:data={data.embed}/>
{/if}
</div>
</div>
<span slot="confirm">Save</span>
</ConfirmationModal>
{/if}
<svelte:window on:keydown={handleKeydown}/>
<script>
import ConfirmationModal from "../ConfirmationModal.svelte";
import Input from "../form/Input.svelte";
import Checkbox from "../form/Checkbox.svelte";
import Textarea from "../form/Textarea.svelte";
import Toggle from "../form/Toggle.svelte";
import EmbedForm from "../EmbedForm.svelte";
import {createEventDispatcher, onMount} from "svelte";
const dispatch = createEventDispatcher();
export let data;
function handleKeydown(e) {
if (e.key === "Escape") {
dispatch("cancel", {});
}
}
onMount(() => {
if (data === undefined) {
data = {
use_embed: false,
};
}
})
</script>
<style>
.body-wrapper {
display: flex;
flex-direction: column;
width: 100%;
}
.row {
display: flex;
flex-direction: row;
gap: 2%;
}
.col {
display: flex;
flex-direction: column;
row-gap: 2vh;
}
.inline {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
gap: 10px;
}
hr {
border-top: 1px solid #777;
border-bottom: 0;
border-left: 0;
border-right: 0;
width: 100%;
flex: 1;
}
</style>

View File

@ -65,7 +65,7 @@
left: 0;
width: 100%;
height: 100%;
z-index: 999;
z-index: 1001;
display: flex;
justify-content: center;
@ -83,7 +83,7 @@
left: 0;
width: 100%;
height: 100%;
z-index: 998;
z-index: 1000;
background-color: #000;
opacity: .5;
}

View File

@ -1,25 +1,28 @@
{#if tagCreateModal}
<TagEditor on:cancel={() => tagCreateModal = false} on:confirm={createTag}/>
{:else if tagEditModal}
<TagEditor bind:data={editData} on:cancel={cancelEdit} on:confirm={editTag}/>
{/if}
<div class="parent">
<div class="content">
<div class="main-col">
<Card footer={false}>
<Card footer footerRight>
<span slot="title">Tags</span>
<div slot="body" class="body-wrapper">
<table class="nice">
<thead>
<tr>
<th>Tag</th>
<th>Edit</th>
<th>Delete</th>
<th style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
{#each Object.entries(tags) as [id, content]}
{#each Object.entries(tags) as [id, tag]}
<tr>
<td>{id}</td>
<td>
<Button type="button" on:click={() => editTag(id)}>Edit</Button>
</td>
<td>
<td class="actions">
<Button type="button" on:click={() => openEditModal(id)}>Edit</Button>
<Button type="button" danger={true} on:click={() => deleteTag(id)}>Delete</Button>
</td>
</tr>
@ -27,27 +30,8 @@
</tbody>
</table>
</div>
</Card>
</div>
<div class="right-col">
<Card footer={false}>
<span slot="title">Create A Tag</span>
<div slot="body" class="body-wrapper">
<form class="body-wrapper" on:submit|preventDefault={createTag}>
<div class="col" style="flex-direction: column">
<Input label="Tag ID" placeholder="mytag" bind:value={createData.id}/>
</div>
<div class="col" style="flex-direction: column">
<Textarea label="Tag Content" placeholder="Enter the text that the bot should respond with"
bind:value={createData.content}/>
</div>
<div class="row" style="justify-content: center">
<div class="col-2">
<Button fullWidth={true} icon="fas fa-plus">Submit</Button>
</div>
</div>
</form>
<div slot="footer">
<Button icon="fas fa-plus" on:click={() => tagCreateModal = true}>Create Tag</Button>
</div>
</Card>
</div>
@ -61,30 +45,89 @@
import axios from "axios";
import {API_URL} from "../js/constants";
import {setDefaultHeaders} from '../includes/Auth.svelte'
import Input from "../components/form/Input.svelte";
import Textarea from "../components/form/Textarea.svelte";
import {fade} from "svelte/transition";
import TagEditor from "../components/manage/TagEditor.svelte";
export let currentRoute;
let guildId = currentRoute.namedParams.id;
let createData = {};
let tags = {};
let editData;
let editId;
function editTag(id) {
createData.id = id;
createData.content = tags[id];
let tagCreateModal = false;
let tagEditModal = false;
function openEditModal(id) {
editId = id;
editData = tags[id];
tagEditModal = true;
}
async function createTag() {
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, createData);
if (res.status !== 200) {
function cancelEdit() {
editId = undefined;
editData = undefined;
tagEditModal = false;
}
async function createTag(e) {
const data = e.detail;
if (!data.id || data.id.length === 0) {
notifyError("Tag ID is required");
return;
}
if (data.content !== null && data.content.length === 0) {
data.content = null;
}
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, data);
if (res.status !== 204) {
notifyError(res.data.error);
return;
}
notifySuccess(`Tag ${createData.id} has been created`);
tags[createData.id] = createData.content;
createData = {};
notifySuccess(`Tag ${data.id} has been created`);
tagCreateModal = false;
tags[data.id] = data;
}
async function editTag(e) {
const data = e.detail;
if (editId !== data.id) {
// Delete old tag
const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: {tag_id: editId}});
if (res.status !== 204) {
notifyError(res.data.error);
return;
}
delete tags[editId];
}
if (!data.id || data.id.length === 0) {
notifyError("Tag ID is required");
return;
}
if (data.content !== null && data.content.length === 0) {
data.content = null;
}
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, data);
if (res.status !== 204) {
notifyError(res.data.error);
return;
}
tags[data.id] = data;
tagEditModal = false;
editData = undefined;
editId = undefined;
notifySuccess("Tag edited successfully");
}
async function deleteTag(id) {
@ -93,14 +136,14 @@
};
const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: data});
if (res.status !== 200) {
if (res.status !== 204) {
notifyError(res.data.error);
return;
}
notifySuccess(`Tag deleted successfully`);
delete tags[id];
tags = tags; // svelte terrible
tags = tags;
}
async function loadTags() {
@ -111,6 +154,9 @@
}
tags = res.data;
for (const id in tags) {
tags[id].use_embed = tags[id].embed !== null;
}
}
withLoadingScreen(async () => {
@ -142,34 +188,23 @@
height: 100%;
}
.right-col {
display: flex;
flex-direction: column;
width: 34%;
height: 100%;
}
.body-wrapper {
display: flex;
flex-direction: column;
row-gap: 2vh;
width: 100%;
height: 100%;
}
.row {
table {
width: 100%;
}
.actions {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
margin-bottom: 2%;
}
.col {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
margin-bottom: 2%;
gap: 10px;
justify-content: flex-end;
}
@media only screen and (max-width: 950px) {
@ -181,9 +216,5 @@
width: 100%;
margin-top: 4%;
}
.right-col {
width: 100%;
}
}
</style>

View File

@ -288,6 +288,7 @@
font-weight: normal;
border-bottom: 1px solid #dee2e6;
padding-left: 10px;
padding-right: 10px;
}
:global(table.nice > thead > tr, table.nice > tbody > tr) {

4
go.mod
View File

@ -6,9 +6,9 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f
github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0
github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8
github.com/apex/log v1.1.2
github.com/getsentry/sentry-go v0.13.0
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607

4
go.sum
View File

@ -41,12 +41,16 @@ github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqS
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f h1:5tpytvC/I1eOgLXhhWvg4RA+vu40oXXzLFwfLde37gY=
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347 h1:JVdXXxs6vxxldfesM/mmBNVBEVfkrFOpjcMdwxJQ4fw=
github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk=
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0 h1:94TYfRYYoAoV3Vyg9hqjtyDFw/Nb5a5+X7ob5rEjjos=
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0/go.mod h1:gThk0bKZTfcKgwpxrFN5BvOgQ6sXoRkz7ojj/9yiU28=
github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8 h1:/hxUoNMsAD1HeG66iPDn0G5zQ8RNltaiL2h50LNplaA=
github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8/go.mod h1:E8y+9Xu8el7QzHALhR/IFvITcJJkDeAxfsBFIEEWJuo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=