mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
feat: refactor ESLint configuration and update dependencies; improve error handling and code structure
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
36
eslint.config.mjs
Normal file
36
eslint.config.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import nextPlugin from '@next/eslint-plugin-next'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import globals from 'globals'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['node_modules', '.next', 'dist', 'coverage'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
nextPlugin.configs['core-web-vitals'],
|
||||
{
|
||||
name: 'react-hooks/custom',
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/purity': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@next/next/no-page-custom-font': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.config.js', '*.config.cjs', '*.config.mjs', 'next.config.js'],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-env node */
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: process.env.BUILD_STATIC ? 'export' : 'standalone',
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -49,7 +49,7 @@
|
||||
"marked": "^15.0.7",
|
||||
"next": "16.1.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"pyodide": "^0.29.1",
|
||||
"pyodide": "^0.27.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
@@ -3450,12 +3450,6 @@
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/emscripten": {
|
||||
"version": "1.41.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
||||
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -7406,12 +7400,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pyodide": {
|
||||
"version": "0.29.1",
|
||||
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.29.1.tgz",
|
||||
"integrity": "sha512-mpk9jtkiM7Ugh1r9P9dbR8vKrmf0lED32hBZq+Fn1kkkBiUoOjSsJEWcyprugICpiFpIXpUOf80ZrvFXkQMk2g==",
|
||||
"license": "MPL-2.0",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.0.tgz",
|
||||
"integrity": "sha512-0O992noCKqv8lPw4+QFWw8d8yrNWVtQ6zPhWNg/RNYbFohiwmV6SGlVh5fWk/4pOxyhgHbayq+ur8JoR/r5U9A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/emscripten": "^1.41.4",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint .",
|
||||
"kill": "fuser -k 5000/tcp"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"marked": "^15.0.7",
|
||||
"next": "16.1.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"pyodide": "^0.29.1",
|
||||
"pyodide": "^0.27.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useAppDispatch } from '@/store/hooks'
|
||||
import { createSnippet, updateSnippet, deleteSnippet } from '@/store/slices/snippetsSlice'
|
||||
import { FloppyDisk, Plus, Pencil, Trash } from '@phosphor-icons/react'
|
||||
import { createSnippet } from '@/store/slices/snippetsSlice'
|
||||
import { FloppyDisk, Plus } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function PersistenceExample() {
|
||||
|
||||
@@ -39,6 +39,7 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps)
|
||||
const result = await analyzeErrorWithAI(errorMessage, errorStack, context)
|
||||
setAnalysis(result)
|
||||
} catch (err) {
|
||||
console.error('AI analysis failed', err)
|
||||
setAnalysisError('Unable to analyze error. The AI service may be temporarily unavailable.')
|
||||
} finally {
|
||||
setIsAnalyzing(false)
|
||||
@@ -105,4 +106,3 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -30,11 +30,7 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
|
||||
const [namespaceToDelete, setNamespaceToDelete] = useState<Namespace | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadNamespaces()
|
||||
}, [])
|
||||
|
||||
const loadNamespaces = async () => {
|
||||
const loadNamespaces = useCallback(async () => {
|
||||
try {
|
||||
const loadedNamespaces = await getAllNamespaces()
|
||||
setNamespaces(loadedNamespaces)
|
||||
@@ -49,7 +45,11 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
|
||||
console.error('Failed to load namespaces:', error)
|
||||
toast.error('Failed to load namespaces')
|
||||
}
|
||||
}
|
||||
}, [onNamespaceChange, selectedNamespaceId])
|
||||
|
||||
useEffect(() => {
|
||||
loadNamespaces()
|
||||
}, [loadNamespaces])
|
||||
|
||||
const handleCreateNamespace = async () => {
|
||||
if (!newNamespaceName.trim()) {
|
||||
|
||||
@@ -59,8 +59,8 @@ export function PythonOutput({ code }: PythonOutputProps) {
|
||||
}, [initializePyodide])
|
||||
|
||||
useEffect(() => {
|
||||
const codeToCheck = code.toLowerCase()
|
||||
setHasInput(codeToCheck.includes('input('))
|
||||
const hasInputCall = /\binput\s*\(/i.test(code)
|
||||
setHasInput(hasInputCall)
|
||||
}, [code])
|
||||
|
||||
const statusTone = initError
|
||||
|
||||
@@ -43,34 +43,24 @@ export function SnippetCard({
|
||||
try {
|
||||
const loadedNamespaces = await getAllNamespaces()
|
||||
setNamespaces(loadedNamespaces)
|
||||
} catch (error) {
|
||||
console.error('Failed to load namespaces:', error)
|
||||
} catch {
|
||||
console.error('Failed to load namespaces')
|
||||
}
|
||||
}
|
||||
|
||||
const snippetData = useMemo(() => {
|
||||
try {
|
||||
const code = snippet?.code || ''
|
||||
const description = snippet?.description || ''
|
||||
const maxLength = appConfig.codePreviewMaxLength
|
||||
const isTruncated = code.length > maxLength
|
||||
const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code
|
||||
const code = snippet?.code || ''
|
||||
const description = snippet?.description || ''
|
||||
const maxLength = appConfig.codePreviewMaxLength
|
||||
const isTruncated = code.length > maxLength
|
||||
const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code
|
||||
|
||||
return {
|
||||
description,
|
||||
displayCode,
|
||||
fullCode: code,
|
||||
isTruncated,
|
||||
hasPreview: snippet?.hasPreview || false
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
description: '',
|
||||
displayCode: '',
|
||||
fullCode: '',
|
||||
isTruncated: false,
|
||||
hasPreview: false
|
||||
}
|
||||
return {
|
||||
description,
|
||||
displayCode,
|
||||
fullCode: code,
|
||||
isTruncated,
|
||||
hasPreview: snippet?.hasPreview || false
|
||||
}
|
||||
}, [snippet])
|
||||
|
||||
@@ -92,6 +82,7 @@ export function SnippetCard({
|
||||
}
|
||||
|
||||
const handleView = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (selectionMode) {
|
||||
handleToggleSelect()
|
||||
} else {
|
||||
@@ -135,7 +126,6 @@ export function SnippetCard({
|
||||
)
|
||||
}
|
||||
|
||||
const currentNamespace = namespaces.find(n => n.id === snippet.namespaceId)
|
||||
const availableNamespaces = namespaces.filter(n => n.id !== snippet.namespaceId)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import * as React from 'react'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AIErrorHelper } from '@/components/error/AIErrorHelper'
|
||||
@@ -15,24 +15,17 @@ interface ReactPreviewProps {
|
||||
}
|
||||
|
||||
export function ReactPreview({ code, language, functionName, inputParameters }: ReactPreviewProps) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [Component, setComponent] = useState<React.ComponentType | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setError(null)
|
||||
setComponent(null)
|
||||
|
||||
const { Component, error } = useMemo(() => {
|
||||
const isReactCode = ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)
|
||||
|
||||
if (!isReactCode) {
|
||||
return
|
||||
return { Component: null, error: null }
|
||||
}
|
||||
|
||||
try {
|
||||
const transformedComponent = transformReactCode(code, functionName)
|
||||
setComponent(() => transformedComponent)
|
||||
return { Component: transformedComponent, error: null }
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to render preview')
|
||||
return { Component: null, error: err instanceof Error ? err.message : 'Failed to render preview' }
|
||||
}
|
||||
}, [code, language, functionName])
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Database, CloudCheck } from '@phosphor-icons/react'
|
||||
import { getStorageConfig } from '@/lib/storage'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
export function BackendIndicator() {
|
||||
const [backend, setBackend] = useState<'indexeddb' | 'flask'>('indexeddb')
|
||||
const [isEnvConfigured, setIsEnvConfigured] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const config = getStorageConfig()
|
||||
setBackend(config.backend)
|
||||
setIsEnvConfigured(Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL))
|
||||
}, [])
|
||||
const { backend } = getStorageConfig()
|
||||
const isEnvConfigured = Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL)
|
||||
|
||||
if (backend === 'indexeddb') {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ComponentShowcase } from '@/components/demo/ComponentShowcase'
|
||||
import { organismsCodeSnippets } from '@/lib/component-code-snippets'
|
||||
import { Snippet } from '@/lib/types'
|
||||
import { NavigationBarsShowcase } from './showcases/NavigationBarsShowcase'
|
||||
import { DataTablesShowcase } from './showcases/DataTablesShowcase'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -8,18 +8,10 @@ import { Label } from '@/components/ui/label';
|
||||
import { Eye, EyeClosed, Key } from '@phosphor-icons/react';
|
||||
|
||||
export function OpenAISettingsCard() {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [apiKey, setApiKey] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('openai_api_key') || '' : ''));
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Load the API key from localStorage on mount
|
||||
const storedKey = localStorage.getItem('openai_api_key');
|
||||
if (storedKey) {
|
||||
setApiKey(storedKey);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSave = () => {
|
||||
if (apiKey.trim()) {
|
||||
localStorage.setItem('openai_api_key', apiKey.trim());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
Bell,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { CSSProperties, ComponentProps, useMemo } from "react"
|
||||
import { CSSProperties, ComponentProps } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
@@ -11,10 +11,8 @@ export function SidebarMenuSkeleton({
|
||||
}: ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
// Use a stable width so skeletons don't change across renders.
|
||||
const width = "70%"
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3,7 +3,10 @@ import { useEffect, useState } from "react"
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = useState<boolean | undefined>(() => {
|
||||
if (typeof window === "undefined") return undefined
|
||||
return window.innerWidth < MOBILE_BREAKPOINT
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
@@ -11,7 +14,6 @@ export function useIsMobile() {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ export function usePythonTerminal() {
|
||||
const [isInitializing, setIsInitializing] = useState(!isPyodideReady())
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [waitingForInput, setWaitingForInput] = useState(false)
|
||||
const [inputPrompt, setInputPrompt] = useState('')
|
||||
const inputResolveRef = useRef<((value: string) => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -42,7 +41,6 @@ export function usePythonTerminal() {
|
||||
|
||||
const handleInputPrompt = (prompt: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
setInputPrompt(prompt)
|
||||
addLine('input-prompt', prompt)
|
||||
setWaitingForInput(true)
|
||||
inputResolveRef.current = resolve
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function createTemplate(snippet: Omit<Snippet, 'id' | 'createdAt' |
|
||||
await createSnippet(template);
|
||||
}
|
||||
|
||||
export async function syncTemplatesFromJSON(templates: any[]): Promise<void> {
|
||||
export async function syncTemplatesFromJSON(templates: unknown[]): Promise<void> {
|
||||
// This would sync predefined templates - implement as needed
|
||||
console.log('Syncing templates', templates.length);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Monaco } from '@monaco-editor/react'
|
||||
* - module: 99 = ModuleKind.ESNext
|
||||
* - jsx: 2 = JsxEmit.React
|
||||
*/
|
||||
export const compilerOptions = {
|
||||
export const compilerOptions: Monaco['languages']['typescript']['CompilerOptions'] = {
|
||||
target: 2, // ScriptTarget.Latest
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: 2, // ModuleResolutionKind.NodeJs
|
||||
@@ -29,7 +29,7 @@ export const compilerOptions = {
|
||||
/**
|
||||
* Diagnostics options for TypeScript/JavaScript validation
|
||||
*/
|
||||
export const diagnosticsOptions = {
|
||||
export const diagnosticsOptions: Monaco['languages']['typescript']['DiagnosticsOptions'] = {
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
}
|
||||
@@ -160,10 +160,10 @@ export const shadcnTypes = `
|
||||
*/
|
||||
export function configureMonacoTypeScript(monaco: Monaco) {
|
||||
// Set compiler options for TypeScript
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions as any)
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions)
|
||||
|
||||
// Set compiler options for JavaScript
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions as any)
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions)
|
||||
|
||||
// Set diagnostics options for TypeScript
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { InputParameter } from '@/lib/types'
|
||||
|
||||
export function parseInputParameters(inputParameters?: InputParameter[]): Record<string, any> {
|
||||
export function parseInputParameters(inputParameters?: InputParameter[]): Record<string, unknown> {
|
||||
if (!inputParameters || inputParameters.length === 0) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const parsedProps: Record<string, any> = {}
|
||||
const parsedProps: Record<string, unknown> = {}
|
||||
|
||||
inputParameters.forEach((param) => {
|
||||
try {
|
||||
|
||||
@@ -46,7 +46,9 @@ sys.stderr = StringIO()
|
||||
`)
|
||||
|
||||
try {
|
||||
const result = pyodide.runPython(code)
|
||||
const result = await pyodide.runPythonAsync(code)
|
||||
|
||||
// Flush and collect stdout/stderr after async execution
|
||||
const stdout = pyodide.runPython('sys.stdout.getvalue()')
|
||||
const stderr = pyodide.runPython('sys.stderr.getvalue()')
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class FlaskStorageAdapter {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
})
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -102,8 +102,8 @@ export class FlaskStorageAdapter {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch snippets: ${response.statusText}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
return data.map((s: any) => ({
|
||||
const data: Snippet[] = await response.json()
|
||||
return data.map((s) => ({
|
||||
...s,
|
||||
createdAt: typeof s.createdAt === 'string' ? new Date(s.createdAt).getTime() : s.createdAt,
|
||||
updatedAt: typeof s.updatedAt === 'string' ? new Date(s.updatedAt).getTime() : s.updatedAt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
type PersistenceConfig,
|
||||
getPersistenceConfig,
|
||||
|
||||
@@ -154,11 +154,11 @@ const snippetsSlice = createSlice({
|
||||
state.items = state.items.filter(s => s.id !== action.payload)
|
||||
})
|
||||
.addCase(moveSnippet.fulfilled, (state, action) => {
|
||||
const { snippetId, targetNamespaceId } = action.payload
|
||||
const { snippetId } = action.payload
|
||||
state.items = state.items.filter(s => s.id !== snippetId)
|
||||
})
|
||||
.addCase(bulkMoveSnippets.fulfilled, (state, action) => {
|
||||
const { snippetIds, targetNamespaceId } = action.payload
|
||||
const { snippetIds } = action.payload
|
||||
state.items = state.items.filter(s => !snippetIds.includes(s.id))
|
||||
state.selectedIds = []
|
||||
state.selectionMode = false
|
||||
|
||||
Reference in New Issue
Block a user