mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-12 18:43:50 +00:00
feat(#35): add new mobile menu
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { cubicInOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let cookiesAccepted;
|
||||
let showBanner = false;
|
||||
@@ -18,7 +20,10 @@
|
||||
</script>
|
||||
|
||||
{#if showBanner}
|
||||
<div class="fixed bottom-0 left-0 w-full bg-gray-800 text-white p-4">
|
||||
<div
|
||||
class="fixed bottom-0 left-0 w-full bg-gray-800 text-white p-4"
|
||||
transition:slide={{ duration: 200, easing: cubicInOut }}
|
||||
>
|
||||
<p class="text-sm">
|
||||
We use cookies to ensure you get the best experience on our website. By continuing, you agree
|
||||
to our use of cookies.
|
||||
|
||||
@@ -1,43 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { cubicInOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let indicator: boolean = false;
|
||||
export let posRight: boolean = false;
|
||||
export let indicator: boolean = false;
|
||||
export let posRight: boolean = false;
|
||||
|
||||
let isOpen = false;
|
||||
let dropdown: HTMLDivElement;
|
||||
|
||||
const DropdownIcon = {
|
||||
let isOpen = false;
|
||||
let dropdown: HTMLDivElement;
|
||||
|
||||
const DropdownIcon = {
|
||||
closed: '▼',
|
||||
open: '▲'
|
||||
};
|
||||
open: '▲'
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!isOpen) return;
|
||||
if (!dropdown.contains(event.target as Node) || event.target instanceof HTMLAnchorElement) {
|
||||
isOpen = false;
|
||||
}
|
||||
};
|
||||
onMount(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!isOpen) return;
|
||||
if (!dropdown.contains(event.target as Node) || event.target instanceof HTMLAnchorElement) {
|
||||
isOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick);
|
||||
document.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={dropdown} class="relative">
|
||||
<div class="dropdown-title">
|
||||
<button class="flex" on:click={() => isOpen = !isOpen}>
|
||||
{#if indicator}
|
||||
<span class="mr-1">{@html isOpen ? DropdownIcon.open : DropdownIcon.closed }</span>
|
||||
{/if}
|
||||
<slot name="dropdown"></slot>
|
||||
</button>
|
||||
</div>
|
||||
<ul class:flex={isOpen} class:hidden={!isOpen} class:right-0={posRight} class="absolute rounded-box min-w-52 shadow z-[1] menu p-2 bg-base-100 text-base-content">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
<div bind:this={dropdown} class="relative block">
|
||||
<div class="dropdown-title block w-full">
|
||||
<button class="flex w-full" on:click={() => (isOpen = !isOpen)}>
|
||||
{#if indicator}
|
||||
<span class="mr-1">{@html isOpen ? DropdownIcon.open : DropdownIcon.closed}</span>
|
||||
{/if}
|
||||
<slot name="dropdown"></slot>
|
||||
</button>
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<ul
|
||||
class:right-0={posRight}
|
||||
class="absolute rounded-box min-w-52 shadow z-[1] m-0 menu p-2 bg-base-100 text-base-content block"
|
||||
transition:slide={{ duration: 200, easing: cubicInOut }}
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+124
-98
@@ -1,110 +1,136 @@
|
||||
<script lang="ts">
|
||||
import type { Session, SupabaseClient } from "@supabase/supabase-js";
|
||||
import MagicLink from "./MagicLink.svelte";
|
||||
import NavLinks from "./NavLinks.svelte";
|
||||
import SignOut from "./SignOut.svelte";
|
||||
import Dropdown from "./Dropdown.svelte";
|
||||
import type { Session, SupabaseClient } from '@supabase/supabase-js';
|
||||
import MagicLink from './MagicLink.svelte';
|
||||
import NavLinks from './NavLinks.svelte';
|
||||
import SignOut from './SignOut.svelte';
|
||||
import Dropdown from './Dropdown.svelte';
|
||||
import { basket, type Basket } from '$lib/stores/basket.js';
|
||||
import { onDestroy } from "svelte";
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let session: Session;
|
||||
export let supabase: SupabaseClient<any, "public", any>;
|
||||
export let session: Session | null;
|
||||
export let supabase: SupabaseClient<any, 'public', any>;
|
||||
|
||||
let isMobileMenuOpen = false;
|
||||
|
||||
function toggleMobileMenu() {
|
||||
isMobileMenuOpen = !isMobileMenuOpen;
|
||||
}
|
||||
|
||||
let localBasket: Basket;
|
||||
let localBasket: Basket;
|
||||
const unsubscribeToBasket = basket.subscribe((value) => {
|
||||
localBasket = value;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeToBasket();
|
||||
});
|
||||
onDestroy(() => {
|
||||
unsubscribeToBasket();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<nav class="bg-primary text-primary-content p-4">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Hamburger Menu Button (Mobile) -->
|
||||
<button on:click={toggleMobileMenu} class="block md:hidden text-white focus:outline-none">
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
|
||||
<NavLinks />
|
||||
|
||||
<!-- Right Side: Basket & User Account -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Basket/Checkout Icon -->
|
||||
<a href="/checkout" class="btn btn-ghost btn-circle">
|
||||
<div class="indicator">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
{#if localBasket.items.length > 0}
|
||||
<span class="badge badge-sm indicator-item">{localBasket.items.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- User Account Dropdown -->
|
||||
<Dropdown posRight>
|
||||
<div slot="dropdown">
|
||||
<div class="btn btn-ghost btn-circle avatar placeholder">
|
||||
<div class="w-10 rounded-full">
|
||||
{#if session?.user}
|
||||
<div class="user-circle text-primary-content border-primary-content">
|
||||
<span class="text-xl">{session?.user.email ? session?.user.email[0].toUpperCase() : '?'}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if session?.user}
|
||||
<li><a href="/account">Account</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><SignOut {supabase} /></li>
|
||||
{:else}
|
||||
<MagicLink {supabase} />
|
||||
{/if}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Hamburger Menu Button (Mobile) -->
|
||||
<div class="lg:hidden mr-4">
|
||||
<Dropdown>
|
||||
<div slot="dropdown">
|
||||
<button class="block text-white focus:outline-none">
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Dropdown content -->
|
||||
<NavLinks isMobile />
|
||||
</Dropdown>
|
||||
</div>
|
||||
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
|
||||
<div class="hidden lg:block">
|
||||
<NavLinks />
|
||||
</div>
|
||||
<!-- Right Side: Basket & User Account -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Basket/Checkout Icon -->
|
||||
<a href="/checkout" class="btn btn-ghost btn-circle hidden lg:flex">
|
||||
<div class="indicator">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
{#if localBasket.items.length > 0}
|
||||
<span class="badge badge-sm indicator-item">{localBasket.items.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
<!-- User Account Dropdown -->
|
||||
<Dropdown posRight>
|
||||
<div slot="dropdown">
|
||||
<div class="btn btn-ghost btn-circle avatar placeholder">
|
||||
<div class="w-10 rounded-full">
|
||||
{#if session?.user}
|
||||
<div class="user-circle text-primary-content border-primary-content">
|
||||
<span class="text-xl"
|
||||
>{session?.user.email ? session?.user.email[0].toUpperCase() : '?'}</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dropdown content -->
|
||||
{#if session?.user}
|
||||
<li class="lg:hidden">
|
||||
<a href="/checkout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
Basket
|
||||
{#if localBasket.items.length > 0}
|
||||
<span class="badge badge-md indicator-item bg-primary text-primary-content"
|
||||
>{localBasket.items.length}</span
|
||||
>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="/account">Account</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><SignOut {supabase} /></li>
|
||||
{:else}
|
||||
<MagicLink {supabase} />
|
||||
{/if}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
|
||||
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
|
||||
import { cubicInOut } from 'svelte/easing';
|
||||
import Dropdown from './Dropdown.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let text: string;
|
||||
export let children: NavLinkItem[] | undefined;
|
||||
export let text: string;
|
||||
export let children: NavLinkItem[] | undefined;
|
||||
export let isMobile: boolean;
|
||||
|
||||
let isOpen = false;
|
||||
</script>
|
||||
|
||||
<Dropdown indicator>
|
||||
<div slot="dropdown">{text}</div>
|
||||
{#each children ?? [] as child}
|
||||
<li><a href={child.href}>{child.text}</a></li>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{#if !isMobile}
|
||||
<Dropdown indicator>
|
||||
<div slot="dropdown">{text}</div>
|
||||
{#each children ?? [] as child}
|
||||
<li><a href={child.href}>{child.text}</a></li>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{:else}
|
||||
<button
|
||||
class:menu-dropdown-show={isOpen}
|
||||
class="menu-dropdown-toggle"
|
||||
on:click={() => (isOpen = !isOpen)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
{#if isOpen}
|
||||
<ul
|
||||
class="menu-dropdown menu-dropdown-show"
|
||||
transition:slide={{ duration: 200, easing: cubicInOut }}
|
||||
>
|
||||
{#each children ?? [] as child}
|
||||
<li><a href={child.href}>{child.text}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
|
||||
import NavLinkParent from './NavLinkParent.svelte';
|
||||
|
||||
export let isMobile = false;
|
||||
|
||||
const links: NavLinkItem[] = [
|
||||
{
|
||||
text: 'Products',
|
||||
href: '/products',
|
||||
href: '/products'
|
||||
},
|
||||
{
|
||||
text: 'Parent',
|
||||
@@ -13,26 +15,26 @@
|
||||
children: [
|
||||
{
|
||||
text: 'Submenu 1',
|
||||
href: '/',
|
||||
href: '/'
|
||||
},
|
||||
{
|
||||
text: 'Submenu 2',
|
||||
href: '/',
|
||||
},
|
||||
],
|
||||
href: '/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Example Product',
|
||||
href: '/tools/exampleproduct',
|
||||
},
|
||||
href: '/tools/exampleproduct'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<ul class="flex space-x-4">
|
||||
<ul class:flex={!isMobile} class:space-x-4={!isMobile}>
|
||||
{#each links as link}
|
||||
<li>
|
||||
{#if link.isParent}
|
||||
<NavLinkParent text={link.text} children={link.children} />
|
||||
<NavLinkParent text={link.text} children={link.children} {isMobile} />
|
||||
{:else}
|
||||
<a href={link.href}>{link.text}</a>
|
||||
{/if}
|
||||
|
||||
+48
-51
@@ -6,13 +6,13 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import CookieConsent from '$lib/components/CookieConsent.svelte';
|
||||
import Nav from '$lib/components/Nav.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicInOut } from 'svelte/easing';
|
||||
|
||||
export let data;
|
||||
let { supabase, session } = data;
|
||||
$: ({ supabase, session } = data);
|
||||
|
||||
|
||||
|
||||
let localGeneral: any;
|
||||
const unsubscribeToGeneralStore = general.subscribe((value) => {
|
||||
localGeneral = value;
|
||||
@@ -40,10 +40,6 @@
|
||||
|
||||
return () => data.subscription.unsubscribe();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -126,52 +122,53 @@
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
<div
|
||||
id={localGeneral.toastType == 'success' ? 'success-toast' : 'error-toast'}
|
||||
role="alert"
|
||||
class="alert {localGeneral.toastType == 'success'
|
||||
? 'alert-success'
|
||||
: 'alert-error'} fixed top-20 right-3 max-w-52 text-white {localGeneral.hideToast
|
||||
? 'hidden'
|
||||
: ''}"
|
||||
>
|
||||
{#if localGeneral.toastType == 'success'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
viewBox="0 0 32 32"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
><g
|
||||
><g id="Error_1_"
|
||||
><g id="Error">
|
||||
<circle cx="16" cy="16" id="BG" r="16" style="fill:#E6E6E6;" />
|
||||
<path
|
||||
d="M14.5,25h3v-3h-3V25z M14.5,6v13h3V6H14.5z"
|
||||
id="Exclamatory_x5F_Sign"
|
||||
style="fill:#D72828;"
|
||||
/>
|
||||
</g>
|
||||
</g></g
|
||||
></svg
|
||||
>
|
||||
{/if}
|
||||
{#if !localGeneral.hideToast}
|
||||
<div
|
||||
id={localGeneral.toastType == 'success' ? 'success-toast' : 'error-toast'}
|
||||
role="alert"
|
||||
class="alert {localGeneral.toastType == 'success'
|
||||
? 'alert-success'
|
||||
: 'alert-error'} fixed top-20 right-3 max-w-52 text-white"
|
||||
transition:slide={{ duration: 200, easing: cubicInOut }}
|
||||
>
|
||||
{#if localGeneral.toastType == 'success'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
viewBox="0 0 32 32"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
><g
|
||||
><g id="Error_1_"
|
||||
><g id="Error">
|
||||
<circle cx="16" cy="16" id="BG" r="16" style="fill:#E6E6E6;" />
|
||||
<path
|
||||
d="M14.5,25h3v-3h-3V25z M14.5,6v13h3V6H14.5z"
|
||||
id="Exclamatory_x5F_Sign"
|
||||
style="fill:#D72828;"
|
||||
/>
|
||||
</g>
|
||||
</g></g
|
||||
></svg
|
||||
>
|
||||
{/if}
|
||||
|
||||
<span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span>
|
||||
</div>
|
||||
<span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.user-circle {
|
||||
|
||||
Reference in New Issue
Block a user