From 95eedb927e6941b7d831aaa15eb499b48f914b56 Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Thu, 30 Oct 2025 17:30:54 +1100 Subject: [PATCH 1/9] Updates to Edge Functions dashboard code editor (#39991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: use mgmt-api's function body endpoint * skip json files from static patterns * set the default content for deno.json * feat: add drag and drop file support to FileExplorerAndEditor - Add drag and drop functionality to accept files - Dropped files are automatically read and added to the files list - Visual feedback with drag overlay during drag operations - Files maintain existing data format with id, name, content, and selected state - Last dropped file is automatically selected for editing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * feat: add binary file handling to FileExplorerAndEditor - Add binary file detection for common file extensions (.wasm, images, executables, etc.) - Show "Cannot Edit Selected File" error message when trying to edit binary files - Binary files dropped via drag-and-drop retain their original binary content - Only show error in editor view, files remain accessible in file list - Text files continue to work normally with full editing capabilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * do not ignore empty files * exclude wasm files from static patterns * Remove eszip parser test as dependency has been removed * Fix TS issues * Fix TS issues * Fix pnpm-lock * Fix * Fix * Nit --------- Co-authored-by: Claude Co-authored-by: Joshen Lim --- .../EdgeFunctionsDiffPanel.tsx | 9 +- .../DeployEdgeFunctionWarningModal.tsx | 2 +- .../FileExplorerAndEditor.utils.ts | 49 +++++ .../{FileExplorerAndEditor.tsx => index.tsx} | 177 +++++++++++++----- .../edge-function-body-query.ts | 78 ++++---- apps/studio/data/fetchers.ts | 2 +- apps/studio/lib/eszip-parser.test.ts | 122 ------------ apps/studio/lib/eszip-parser.ts | 144 -------------- apps/studio/package.json | 2 +- apps/studio/pages/api/edge-functions/body.ts | 91 --------- .../[ref]/functions/[functionSlug]/code.tsx | 56 +++--- .../pages/project/[ref]/functions/new.tsx | 6 +- packages/api-types/types/platform.d.ts | 78 +++++++- pnpm-lock.yaml | 36 +--- 14 files changed, 352 insertions(+), 500 deletions(-) create mode 100644 apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.utils.ts rename apps/studio/components/ui/FileExplorerAndEditor/{FileExplorerAndEditor.tsx => index.tsx} (58%) delete mode 100644 apps/studio/lib/eszip-parser.test.ts delete mode 100644 apps/studio/lib/eszip-parser.ts delete mode 100644 apps/studio/pages/api/edge-functions/body.ts diff --git a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx index d91ae01541efc..2865b092f7b75 100644 --- a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx @@ -14,7 +14,6 @@ import { basename } from 'path' import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui' const EMPTY_FUNCTION_BODY: EdgeFunctionBodyData = { - version: 0, files: EMPTY_ARR, } @@ -95,8 +94,12 @@ const FunctionDiff = ({ const language = useMemo(() => { if (!activeFileKey) return 'plaintext' - if (activeFileKey.endsWith('.ts') || activeFileKey.endsWith('.tsx')) return 'typescript' - if (activeFileKey.endsWith('.js') || activeFileKey.endsWith('.jsx')) return 'javascript' + if (activeFileKey.endsWith('.ts') || activeFileKey.endsWith('.tsx')) { + return 'typescript' + } + if (activeFileKey.endsWith('.js') || activeFileKey.endsWith('.jsx')) { + return 'javascript' + } if (activeFileKey.endsWith('.json')) return 'json' if (activeFileKey.endsWith('.sql')) return 'sql' return 'plaintext' diff --git a/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal.tsx b/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal.tsx index dd143783bab6e..06ef8e594125b 100644 --- a/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal.tsx +++ b/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal.tsx @@ -17,7 +17,7 @@ export const DeployEdgeFunctionWarningModal = ({ { + const extension = fileName.split('.').pop()?.toLowerCase() + const binaryExtensions = [ + 'wasm', + 'jpg', + 'jpeg', + 'png', + 'gif', + 'bmp', + 'ico', + 'svg', + 'mp3', + 'mp4', + 'avi', + 'mov', + 'zip', + 'rar', + '7z', + 'tar', + 'gz', + 'bz2', + 'pdf', + ] + return binaryExtensions.includes(extension || '') +} + +export const getLanguageFromFileName = (fileName: string): string => { + const extension = fileName.split('.').pop()?.toLowerCase() + switch (extension) { + case 'ts': + case 'tsx': + return 'typescript' + case 'js': + case 'jsx': + return 'javascript' + case 'json': + return 'json' + case 'html': + return 'html' + case 'css': + return 'css' + case 'md': + return 'markdown' + case 'csv': + return 'csv' + default: + return 'plaintext' // Default to plaintext + } +} diff --git a/apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx b/apps/studio/components/ui/FileExplorerAndEditor/index.tsx similarity index 58% rename from apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx rename to apps/studio/components/ui/FileExplorerAndEditor/index.tsx index f425a22b38eec..f1988cff2aa7d 100644 --- a/apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx +++ b/apps/studio/components/ui/FileExplorerAndEditor/index.tsx @@ -1,3 +1,4 @@ +import { AnimatePresence, motion } from 'framer-motion' import { Edit, File, Plus, Trash } from 'lucide-react' import { useEffect, useState } from 'react' @@ -13,6 +14,7 @@ import { TreeView, TreeViewItem, } from 'ui' +import { getLanguageFromFileName, isBinaryFile } from './FileExplorerAndEditor.utils' interface FileData { id: number @@ -32,35 +34,16 @@ interface FileExplorerAndEditorProps { } } -const getLanguageFromFileName = (fileName: string): string => { - const extension = fileName.split('.').pop()?.toLowerCase() - switch (extension) { - case 'ts': - case 'tsx': - return 'typescript' - case 'js': - case 'jsx': - return 'javascript' - case 'json': - return 'json' - case 'html': - return 'html' - case 'css': - return 'css' - case 'md': - return 'markdown' - default: - return 'typescript' // Default to typescript - } -} +const denoJsonDefaultContent = JSON.stringify({ imports: {} }, null, '\t') -const FileExplorerAndEditor = ({ +export const FileExplorerAndEditor = ({ files, onFilesChange, aiEndpoint, aiMetadata, }: FileExplorerAndEditorProps) => { const selectedFile = files.find((f) => f.selected) ?? files[0] + const [isDragOver, setIsDragOver] = useState(false) const [treeData, setTreeData] = useState({ name: '', @@ -95,9 +78,55 @@ const FileExplorerAndEditor = ({ ]) } + const addDroppedFiles = async (droppedFiles: FileList) => { + const newFiles: FileData[] = [] + const updatedFiles = files.map((f) => ({ ...f, selected: false })) + + for (let i = 0; i < droppedFiles.length; i++) { + const file = droppedFiles[i] + const newId = Math.max(0, ...files.map((f) => f.id), ...newFiles.map((f) => f.id)) + 1 + + try { + let content: string + if (isBinaryFile(file.name)) { + // For binary files, read as ArrayBuffer and convert to base64 or keep as binary data + const arrayBuffer = await file.arrayBuffer() + const bytes = new Uint8Array(arrayBuffer) + content = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') + } else { + content = await file.text() + } + + newFiles.push({ + id: newId, + name: file.name, + content, + selected: i === droppedFiles.length - 1, // Select the last dropped file + }) + } catch (error) { + console.error(`Failed to read file ${file.name}:`, error) + } + } + + if (newFiles.length > 0) { + onFilesChange([...updatedFiles, ...newFiles]) + } + } + const handleFileNameChange = (id: number, newName: string) => { if (!newName.trim()) return // Don't allow empty names - const updatedFiles = files.map((file) => (file.id === id ? { ...file, name: newName } : file)) + const updatedFiles = files.map((file) => + file.id === id + ? { + ...file, + name: newName, + content: + newName === 'deno.json' && file.content === '' + ? denoJsonDefaultContent + : file.content, + } + : file + ) onFilesChange(updatedFiles) } @@ -145,6 +174,26 @@ const FileExplorerAndEditor = ({ setTreeData(updatedTreeData) } + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(false) + } + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(false) + + const droppedFiles = e.dataTransfer.files + if (droppedFiles.length > 0) { + await addDroppedFiles(droppedFiles) + } + } + // Update treeData when files change useEffect(() => { setTreeData({ @@ -161,7 +210,27 @@ const FileExplorerAndEditor = ({ }, [files]) return ( -
+
+ + {isDragOver && ( + +
+
Drop files here to add them
+
+
+ )} +

@@ -197,13 +266,17 @@ const FileExplorerAndEditor = ({ icon={} isEditing={Boolean(element.metadata?.isEditing)} onEditSubmit={(value) => { - if (originalId !== null) handleFileNameChange(originalId, value) + if (originalId !== null) { + handleFileNameChange(originalId, value) + } }} onClick={() => { if (originalId !== null) handleFileSelect(originalId) }} onDoubleClick={() => { - if (originalId !== null) handleStartRename(originalId) + if (originalId !== null) { + handleStartRename(originalId) + } }} />

@@ -226,7 +299,9 @@ const FileExplorerAndEditor = ({ { - if (originalId !== null) handleFileDelete(originalId) + if (originalId !== null) { + handleFileDelete(originalId) + } }} onFocusCapture={(e) => e.stopPropagation()} > @@ -243,26 +318,36 @@ const FileExplorerAndEditor = ({
- + {selectedFile && isBinaryFile(selectedFile.name) ? ( +
+
+
Cannot Edit Selected File
+
+ Binary files like .{selectedFile.name.split('.').pop()} cannot be edited in the text + editor +
+
+
+ ) : ( + + )}
) } - -export default FileExplorerAndEditor diff --git a/apps/studio/data/edge-functions/edge-function-body-query.ts b/apps/studio/data/edge-functions/edge-function-body-query.ts index 2f9b921ad2e14..7fe5947f63393 100644 --- a/apps/studio/data/edge-functions/edge-function-body-query.ts +++ b/apps/studio/data/edge-functions/edge-function-body-query.ts @@ -1,6 +1,7 @@ +import { getMultipartBoundary, parseMultipartStream } from '@mjackson/multipart-parser' import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { constructHeaders, fetchHandler, handleError } from 'data/fetchers' -import { BASE_PATH, IS_PLATFORM } from 'lib/constants' +import { get, handleError } from 'data/fetchers' +import { IS_PLATFORM } from 'lib/constants' import { ResponseError } from 'types' import { edgeFunctionsKeys } from './keys' @@ -15,10 +16,29 @@ export type EdgeFunctionFile = { } export type EdgeFunctionBodyResponse = { - version: number files: EdgeFunctionFile[] } +async function streamToString(stream: ReadableStream) { + const reader = stream.getReader() + const decoder = new TextDecoder() + let result = '' + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + result += decoder.decode(value, { stream: true }) + } + // Final decode to handle any remaining bytes + result += decoder.decode() + return result + } catch (error) { + console.error('Error reading stream:', error) + throw error + } +} + export async function getEdgeFunctionBody( { projectRef, slug }: EdgeFunctionBodyVariables, signal?: AbortSignal @@ -26,41 +46,31 @@ export async function getEdgeFunctionBody( if (!projectRef) throw new Error('projectRef is required') if (!slug) throw new Error('slug is required') - try { - // Get authorization headers - const headers = await constructHeaders({ - 'Content-Type': 'application/json', - }) + const { data, response, error } = await get('/v1/projects/{ref}/functions/{function_slug}/body', { + params: { path: { ref: projectRef, function_slug: slug } }, + headers: { Accept: 'multipart/form-data' }, + parseAs: 'stream', + signal, + }) - // Send to our API for processing (the API will handle the fetch from v1 endpoint) - const parseResponse = await fetchHandler(`${BASE_PATH}/api/edge-functions/body`, { - method: 'POST', - body: JSON.stringify({ projectRef, slug }), - headers, - credentials: 'include', - signal, - }) + if (error) handleError(error) - if (!parseResponse.ok) { - const { error } = await parseResponse.json() - handleError( - typeof error === 'object' - ? error - : typeof error === 'string' - ? { message: error } - : { message: 'Unknown error' } - ) - } + const contentTypeHeader = response.headers.get('content-type') ?? '' + const boundary = getMultipartBoundary(contentTypeHeader) + const files = [] - const response = (await parseResponse.json()) as EdgeFunctionBodyResponse - return response - } catch (error) { - handleError(error) - return { - version: 0, - files: [], - } as EdgeFunctionBodyResponse + if (!data || !boundary) return { files: [] } + + for await (let part of parseMultipartStream(data, { boundary })) { + if (part.isFile) { + files.push({ + name: part.filename, + content: part.text, + }) + } } + + return { files: files as EdgeFunctionFile[] } } export type EdgeFunctionBodyData = Awaited> diff --git a/apps/studio/data/fetchers.ts b/apps/studio/data/fetchers.ts index 6b2969b8217fe..aa8d66e428c96 100644 --- a/apps/studio/data/fetchers.ts +++ b/apps/studio/data/fetchers.ts @@ -24,7 +24,7 @@ export const fetchHandler: typeof fetch = async (input, init) => { } } -const client = createClient({ +export const client = createClient({ fetch: fetchHandler, // [Joshen] Just FYI, the replace is temporary until we update env vars API_URL to remove /platform or /v1 - should just be the base URL baseUrl: API_URL?.replace('/platform', ''), diff --git a/apps/studio/lib/eszip-parser.test.ts b/apps/studio/lib/eszip-parser.test.ts deleted file mode 100644 index ec38531a19d73..0000000000000 --- a/apps/studio/lib/eszip-parser.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Parser } from '@deno/eszip' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { parseEszip } from './eszip-parser' - -vi.mock('@deno/eszip', () => ({ - Parser: { - createInstance: vi.fn(), - }, -})) - -vi.stubGlobal( - 'File', - class MockFile { - name: string - content: string - - constructor(content: string[], name: string) { - this.name = name - this.content = content[0] - } - - async text() { - return this.content - } - } -) - -vi.stubGlobal( - 'URL', - class MockURL { - pathname: string - - constructor(url: string) { - this.pathname = url - } - } -) - -describe('eszip-parser', () => { - const mockParser = { - parseBytes: vi.fn(), - load: vi.fn(), - getModuleSource: vi.fn(), - } - - beforeEach(() => { - vi.clearAllMocks() - ;(Parser.createInstance as any).mockResolvedValue(mockParser) - }) - - afterEach(() => { - vi.resetAllMocks() - }) - - describe('parseEszip', () => { - it('should successfully parse and extract files from eszip', async () => { - const mockBytes = new Uint8Array([1, 2, 3]) - const mockSpecifiers = ['file1.ts', 'file2.ts'] - const mockModuleSource1 = 'export const hello = "world"' - const mockModuleSource2 = 'export const foo = "bar"' - - mockParser.parseBytes.mockResolvedValue(mockSpecifiers) - mockParser.load.mockResolvedValue(undefined) - mockParser.getModuleSource - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(mockModuleSource1) - .mockResolvedValueOnce(mockModuleSource2) - - const result = await parseEszip(mockBytes) - - expect(Parser.createInstance).toHaveBeenCalledTimes(1) - expect(mockParser.parseBytes).toHaveBeenCalledWith(mockBytes) - expect(mockParser.load).toHaveBeenCalled() - expect(result.version).toEqual(0) - expect(result.files).toHaveLength(2) - expect(result.files[0]).toEqual({ - name: 'file1.ts', - content: mockModuleSource1, - }) - expect(result.files[1]).toEqual({ - name: 'file2.ts', - content: mockModuleSource2, - }) - }) - - it('should handle parseBytes failure', async () => { - mockParser.parseBytes.mockRejectedValue(new Error('Parse error')) - await expect(parseEszip(new Uint8Array())).rejects.toThrow('Parse error') - }) - - it('should handle load failure', async () => { - mockParser.parseBytes.mockResolvedValue(['file1.ts']) - mockParser.load.mockRejectedValue(new Error('Load error')) - - await expect(parseEszip(new Uint8Array())).rejects.toThrow('Load error') - }) - - it('should filter out unwanted specifiers', async () => { - const mockBytes = new Uint8Array([1, 2, 3]) - const mockSpecifiers = [ - 'file1.ts', - 'npm:package', - 'https://example.com/file.ts', - 'file2.ts', - '---internal', - 'jsr:package', - ] - const mockModuleSource = 'export const test = "test"' - - mockParser.parseBytes.mockResolvedValue(mockSpecifiers) - mockParser.load.mockResolvedValue(undefined) - mockParser.getModuleSource.mockResolvedValue(mockModuleSource) - - const result = await parseEszip(mockBytes) - // Only file1.ts and file2.ts should be included - expect(result.version).toEqual(0) - expect(result.files).toHaveLength(2) - expect(result.files[0].name).toBe('file1.ts') - expect(result.files[1].name).toBe('file2.ts') - }) - }) -}) diff --git a/apps/studio/lib/eszip-parser.ts b/apps/studio/lib/eszip-parser.ts deleted file mode 100644 index 7aa5428e7478e..0000000000000 --- a/apps/studio/lib/eszip-parser.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Parser } from '@deno/eszip' -import path from 'path' - -function url2path(url: string) { - try { - // Parse the URL - return new URL(url).pathname - } catch (error) { - // If URL parsing fails, fallback to extracting just the filename - console.warn('Failed to parse URL:', url) - try { - // Try to extract just the filename part - const parts = url.split('/').filter(Boolean) - if (parts.length > 0) { - return parts[parts.length - 1] // Return just the filename - } - } catch (e) { - // Last resort: use the original path joining - console.error('Failed to extract filename:', e) - } - return path.join(...new URL(url).pathname.split('/').filter(Boolean)) - } -} - -// Initialize parser outside of request handler -let parserPromise: Promise | null = null - -async function getParser() { - if (!parserPromise) { - parserPromise = Parser.createInstance().catch((err) => { - console.error('Failed to create parser instance:', err) - parserPromise = null - throw err - }) - } - return parserPromise -} - -export async function parseEszip(bytes: Uint8Array) { - try { - const parser = await getParser() - if (!parser) { - throw new Error('Failed to initialize parser') - } - - // Parse bytes in a try-catch block - let specifiers: string[] = [] - try { - specifiers = await parser.parseBytes(bytes) - } catch (parseError) { - console.error('Error parsing bytes:', parseError) - // Reset parser on parse error - parserPromise = null - throw parseError - } - - // Load in a separate try-catch - try { - await parser.load() - } catch (loadError) { - console.error('Error loading parser:', loadError) - parserPromise = null - throw loadError - } - - // Extract version - let version = parseInt(await parser.getModuleSource('---SUPABASE-ESZIP-VERSION-ESZIP---')) - if (isNaN(version)) { - version = 0 - } - - // Extract files from the eszip - const files = await extractEszip(parser, specifiers, version >= 2) - - // Convert files to the expected format - const responseFiles = await Promise.all( - files.map(async (file) => { - const content = await file.text() - return { - name: file.name, - content: content, - } - }) - ) - - return { - version, - files: responseFiles, - } - } catch (error) { - console.error('Error in parseEszip:', error) - throw error - } -} - -async function extractEszip(parser: any, specifiers: string[], isDeno2: boolean) { - const files = [] - - // First, filter out the specifiers we want to keep - const filteredSpecifiers = specifiers.filter((specifier) => { - const shouldSkip = - specifier.startsWith('---') || - specifier.startsWith('npm:') || - specifier.startsWith('static:') || - specifier.startsWith('vfs:') || - specifier.startsWith('https:') || - specifier.startsWith('jsr:') - - if (shouldSkip) { - console.log('Skipping specifier:', specifier) - } else { - console.log('Keeping specifier:', specifier) - } - - return !shouldSkip - }) - - console.log('Filtered specifiers count:', filteredSpecifiers.length) - console.log('Filtered specifiers:', JSON.stringify(filteredSpecifiers)) - - // Then process each one - for (const specifier of filteredSpecifiers) { - try { - // Try to get the module source - const moduleSource = await parser.getModuleSource(specifier) - let qualifiedSpecifier = specifier - - // Get the file path - if (isDeno2 && !specifier.startsWith('file://')) { - qualifiedSpecifier = `file://${specifier}` - } - const filePath = url2path(qualifiedSpecifier) - - // Create a file object - const file = new File([moduleSource], filePath) - - files.push(file) - } catch (error) { - console.error('Error processing specifier:', specifier, error) - } - } - - return files -} diff --git a/apps/studio/package.json b/apps/studio/package.json index 2518fbf3c5f37..35535056282f3 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -32,7 +32,6 @@ "@ai-sdk/react": "2.0.45", "@aws-sdk/credential-providers": "^3.804.0", "@dagrejs/dagre": "^1.0.4", - "@deno/eszip": "0.83.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^8.0.0", @@ -45,6 +44,7 @@ "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.1.3", "@hookform/resolvers": "^3.1.1", + "@mjackson/multipart-parser": "^0.10.1", "@modelcontextprotocol/sdk": "^1.18.0", "@monaco-editor/react": "^4.6.0", "@next/bundle-analyzer": "15.3.1", diff --git a/apps/studio/pages/api/edge-functions/body.ts b/apps/studio/pages/api/edge-functions/body.ts deleted file mode 100644 index ed220a07144ed..0000000000000 --- a/apps/studio/pages/api/edge-functions/body.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { API_URL } from 'lib/constants' -import { parseEszip } from 'lib/eszip-parser' -import { NextApiRequest, NextApiResponse } from 'next' - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { method } = req - - switch (method) { - case 'POST': - return handlePost(req, res) - default: - return new Response( - JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }), - { - status: 405, - headers: { 'Content-Type': 'application/json', Allow: 'POST' }, - } - ) - } -} - -async function handlePost(req: NextApiRequest, res: NextApiResponse) { - try { - const { projectRef, slug } = req.body || {} - - if (!projectRef) { - return res.status(400).json({ error: 'projectRef is required' }) - } - if (!slug) { - return res.status(400).json({ error: 'slug is required' }) - } - - // Get authorization token from the request - const authToken = req.headers.authorization - - if (!authToken) { - return res.status(401).json({ error: 'No authorization token was found' }) - } - - // Fetch the eszip data - const headers = new Headers() - headers.set('Accept', 'application/octet-stream') - headers.set('Authorization', typeof authToken === 'string' ? authToken : authToken[0]) - - // Forward other important headers - if (req.headers.cookie) { - headers.set('Cookie', req.headers.cookie) - } - - const baseUrl = API_URL?.replace('/platform', '') - const url = `${baseUrl}/v1/projects/${projectRef}/functions/${slug}/body` - - const response = await fetch(url, { - method: 'GET', - headers, - credentials: 'include', - referrerPolicy: 'no-referrer-when-downgrade', - }) - - if (!response.ok) { - const error = await response.json() - return res.status(response.status).json(error) - } - - // Verify content type is binary/eszip - const contentType = response.headers.get('content-type') - if (!contentType || !contentType.includes('application/octet-stream')) { - return res.status(400).json({ - error: - 'Invalid response: Expected eszip file but received ' + (contentType || 'unknown format'), - }) - } - - // Get the eszip data as ArrayBuffer - const arrayBuffer = await response.arrayBuffer() - - if (arrayBuffer.byteLength === 0) { - return res.status(400).json({ error: 'Invalid eszip: File is empty' }) - } - - const uint8Array = new Uint8Array(arrayBuffer) - - // Parse the eszip file using our utility - const parsed = await parseEszip(uint8Array) - - return res.status(200).json(parsed) - } catch (error) { - console.error('Error processing edge function body:', error) - return res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx index 2de8d35300607..910cf24f35e97 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx @@ -9,7 +9,7 @@ import { DeployEdgeFunctionWarningModal } from 'components/interfaces/EdgeFuncti import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import FileExplorerAndEditor from 'components/ui/FileExplorerAndEditor/FileExplorerAndEditor' +import { FileExplorerAndEditor } from 'components/ui/FileExplorerAndEditor' import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation' @@ -30,7 +30,10 @@ const CodePage = () => { const { can: canDeployFunction } = useAsyncCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') - const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug }) + const { data: selectedFunction } = useEdgeFunctionQuery({ + projectRef: ref, + slug: functionSlug, + }) const { data: functionBody, isLoading: isLoadingFiles, @@ -110,6 +113,9 @@ const CodePage = () => { import_map_path: files.some(({ name }) => name === newImportMapPath) ? newImportMapPath : fallbackImportMapPath(), + static_patterns: files + .filter(({ name }) => !name.match(/\.(js|ts|jsx|tsx|json|wasm)$/i)) + .map(({ name }) => name), }, files: files.map(({ name, content }) => ({ name, content })), }) @@ -120,31 +126,23 @@ const CodePage = () => { } } - function getBasePath( - entrypoint: string | undefined, - fileNames: string[], - version: number - ): string { + function getBasePath(entrypoint: string | undefined, fileNames: string[]): string { if (!entrypoint) { return '/' } - let qualifiedEntrypoint = entrypoint + let candidate = fileNames.find((name) => entrypoint.endsWith(name)) - if (version >= 2) { - const candidate = fileNames.find((name) => entrypoint.endsWith(name)) - if (candidate) { - qualifiedEntrypoint = `file://${candidate}` - } else { - qualifiedEntrypoint = entrypoint + if (candidate) { + return dirname(candidate) + } else { + try { + return dirname(new URL(entrypoint).pathname) + } catch (e) { + console.error('Failed to parse entrypoint', entrypoint) + return '/' } } - try { - return dirname(new URL(qualifiedEntrypoint).pathname) - } catch (e) { - console.error('Failed to parse entrypoint', qualifiedEntrypoint) - return '/' - } } const handleDeployClick = () => { @@ -152,14 +150,20 @@ const CodePage = () => { setShowDeployWarning(true) sendEvent({ action: 'edge_function_deploy_updates_button_clicked', - groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, + groups: { + project: ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) } const handleDeployConfirm = () => { sendEvent({ action: 'edge_function_deploy_updates_confirm_clicked', - groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, + groups: { + project: ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) onUpdate() } @@ -169,12 +173,9 @@ const CodePage = () => { if (selectedFunction?.entrypoint_path && functionBody) { const base_path = getBasePath( selectedFunction?.entrypoint_path, - functionBody.files.map((file) => file.name), - functionBody.version + functionBody.files.map((file) => file.name) ) const filesWithRelPath = functionBody.files - // ignore empty files - .filter((file: { name: string; content: string }) => !!file.content.length) // set file paths relative to entrypoint .map((file: { name: string; content: string }) => { try { @@ -185,7 +186,8 @@ const CodePage = () => { return file } - file.name = relative(base_path, file.name) + // prepend "/" to turn relative paths to absolute + file.name = relative('/' + base_path, '/' + file.name) return file } catch (e) { console.error(e) diff --git a/apps/studio/pages/project/[ref]/functions/new.tsx b/apps/studio/pages/project/[ref]/functions/new.tsx index b813c3babb6be..507a532cfe0ee 100644 --- a/apps/studio/pages/project/[ref]/functions/new.tsx +++ b/apps/studio/pages/project/[ref]/functions/new.tsx @@ -11,7 +11,8 @@ import { EDGE_FUNCTION_TEMPLATES } from 'components/interfaces/Functions/Functio import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import FileExplorerAndEditor from 'components/ui/FileExplorerAndEditor/FileExplorerAndEditor' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { FileExplorerAndEditor } from 'components/ui/FileExplorerAndEditor' import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -19,6 +20,7 @@ import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { AiIconAnimation, Button, @@ -42,8 +44,6 @@ import { TooltipContent, TooltipTrigger, } from 'ui' -import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' // Array of adjectives and nouns for random function name generation const ADJECTIVES = [ diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 3d3762b524626..802ab87b7d34c 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -3764,7 +3764,11 @@ export interface paths { } get?: never put?: never - post?: never + /** + * Update publication for source + * @description Update a publication for a source. Requires bearer auth and an active, healthy project. + */ + post: operations['ReplicationSourcesController_updatePublication'] /** * Delete publication for source * @description Delete a publication for a source. Requires bearer auth and an active, healthy project. @@ -10062,6 +10066,21 @@ export interface components { */ version_id: number } + UpdateReplicationPublicationBody: { + /** @description Publication tables */ + tables: { + /** + * @description Table name + * @example orders + */ + name: string + /** + * @description Table schema + * @example public + */ + schema: string + }[] + } UpdateSecretsConfigBody: { change_tracking_id: string jwt_secret: string @@ -23587,6 +23606,63 @@ export interface operations { } } } + ReplicationSourcesController_updatePublication: { + parameters: { + query?: never + header?: never + path: { + /** @description Publication name */ + publication_name: string + /** @description Project ref */ + ref: string + /** @description Source id */ + source_id: number + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateReplicationPublicationBody'] + } + } + responses: { + /** @description Publication updated. */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Forbidden action */ + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Unexpected error while updating publication. */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } ReplicationSourcesController_deletePublication: { parameters: { query?: never diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e90cc80e0b287..49e35824adb69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,7 +397,7 @@ importers: version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^10.3.0 - version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' version: 2.75.1 @@ -750,9 +750,6 @@ importers: '@dagrejs/dagre': specifier: ^1.0.4 version: 1.0.4 - '@deno/eszip': - specifier: 0.83.0 - version: 0.83.0 '@dnd-kit/core': specifier: ^6.1.0 version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -789,6 +786,9 @@ importers: '@hookform/resolvers': specifier: ^3.1.1 version: 3.3.1(react-hook-form@7.47.0(react@18.3.1)) + '@mjackson/multipart-parser': + specifier: ^0.10.1 + version: 0.10.1 '@modelcontextprotocol/sdk': specifier: ^1.18.0 version: 1.18.0(supports-color@8.1.1) @@ -812,7 +812,7 @@ importers: version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^10.3.0 - version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@std/path': specifier: npm:@jsr/std__path@^1.0.8 version: '@jsr/std__path@1.0.8' @@ -1227,7 +1227,7 @@ importers: version: 2.11.3(@types/node@22.13.14)(typescript@5.9.2) next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) node-mocks-http: specifier: ^1.17.2 version: 1.17.2(@types/node@22.13.14) @@ -1591,7 +1591,7 @@ importers: version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^10 - version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' version: 2.75.1 @@ -2576,7 +2576,7 @@ importers: version: link:../api-types next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) tsx: specifier: 'catalog:' version: 4.20.3 @@ -3429,15 +3429,9 @@ packages: '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} - '@deno/eszip@0.83.0': - resolution: {integrity: sha512-gTKYMQ+uv20IUJuEBYkjovMPflFjX7caJ8cwA/sZVqic0L/PFP2gZMFt/GiCHc8eVejhlJLGxg0J4qehDq/f2A==} - '@deno/shim-deno-test@0.5.0': resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==} - '@deno/shim-deno@0.18.2': - resolution: {integrity: sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA==} - '@deno/shim-deno@0.19.2': resolution: {integrity: sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q==} @@ -21852,18 +21846,8 @@ snapshots: '@date-fns/tz@1.2.0': {} - '@deno/eszip@0.83.0': - dependencies: - '@deno/shim-deno': 0.18.2 - undici: 6.21.2 - '@deno/shim-deno-test@0.5.0': {} - '@deno/shim-deno@0.18.2': - dependencies: - '@deno/shim-deno-test': 0.5.0 - which: 4.0.0 - '@deno/shim-deno@0.19.2': dependencies: '@deno/shim-deno-test': 0.5.0 @@ -27630,7 +27614,7 @@ snapshots: '@sentry/core@10.3.0': {} - '@sentry/nextjs@10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)': + '@sentry/nextjs@10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.36.0 @@ -36735,7 +36719,7 @@ snapshots: dependencies: js-yaml-loader: 1.2.2 - next-router-mock@0.9.13(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1): + next-router-mock@0.9.13(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1): dependencies: next: 15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) react: 18.3.1 From faf8107d69c7a320f352fbbb5780bd112fd54771 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Thu, 30 Oct 2025 03:03:09 -0400 Subject: [PATCH 2/9] feat(auth): add account changes notifications template schemas (#39899) * feat(auth): add account changes notifications template schemas * add ability to enable/disable the security templates * layout tweaks * misc padding and spacing * filter out new templates for non-preview users * Fix premature error state when loading templates page --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com> Co-authored-by: Joshen Lim --- .../Auth/AuthTemplatesValidation.tsx | 265 ++++++++++++++++-- .../Auth/EmailTemplates/EmailTemplates.tsx | 244 +++++++++++++--- .../Auth/EmailTemplates/SpamValidation.tsx | 3 +- .../Auth/EmailTemplates/TemplateEditor.tsx | 4 +- .../[ref]/auth/templates/[templateId].tsx | 31 +- apps/studio/types/form.ts | 1 + 6 files changed, 463 insertions(+), 85 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx b/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx index bf09377ebed02..7d35614d2e9dd 100644 --- a/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx @@ -1,6 +1,4 @@ import { object, string } from 'yup' - -import { DOCS_URL } from 'lib/constants' import type { FormSchema } from 'types' const JSON_SCHEMA_VERSION = 'http://json-schema.org/draft-07/schema#' @@ -32,12 +30,10 @@ const CONFIRMATION: FormSchema = { }, }, validationSchema: object().shape({ - MAILER_SUBJECTS_CONFIRMATION: string().required('"Subject heading is required.'), + MAILER_SUBJECTS_CONFIRMATION: string().required('Subject heading is required.'), }), misc: { - iconKey: 'email-icon2', - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. -[Learn more](${DOCS_URL}/guides/auth/social-login/auth-apple#configure-your-services-id)`, + emailTemplateType: 'authentication', }, } @@ -68,12 +64,10 @@ const INVITE: FormSchema = { }, }, validationSchema: object().shape({ - MAILER_SUBJECTS_INVITE: string().required('"Subject heading is required.'), + MAILER_SUBJECTS_INVITE: string().required('Subject heading is required.'), }), misc: { - iconKey: 'email-icon2', - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. -[Learn more](${DOCS_URL}/guides/auth/social-login/auth-apple#configure-your-services-id)`, + emailTemplateType: 'authentication', }, } @@ -104,12 +98,10 @@ const MAGIC_LINK: FormSchema = { }, }, validationSchema: object().shape({ - MAILER_SUBJECTS_MAGIC_LINK: string().required('"Subject heading is required.'), + MAILER_SUBJECTS_MAGIC_LINK: string().required('Subject heading is required.'), }), misc: { - iconKey: 'email-icon2', - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. -[Learn more](${DOCS_URL}/guides/auth/social-login/auth-apple#configure-your-services-id)`, + emailTemplateType: 'authentication', }, } @@ -141,12 +133,10 @@ const EMAIL_CHANGE: FormSchema = { }, }, validationSchema: object().shape({ - MAILER_SUBJECTS_EMAIL_CHANGE: string().required('"Subject heading is required.'), + MAILER_SUBJECTS_EMAIL_CHANGE: string().required('Subject heading is required.'), }), misc: { - iconKey: 'email-icon2', - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. -[Learn more](${DOCS_URL}/guides/auth/social-login/auth-apple#configure-your-services-id)`, + emailTemplateType: 'authentication', }, } @@ -177,12 +167,10 @@ const RECOVERY: FormSchema = { }, }, validationSchema: object().shape({ - MAILER_SUBJECTS_RECOVERY: string().required('"Subject heading is required.'), + MAILER_SUBJECTS_RECOVERY: string().required('Subject heading is required.'), }), misc: { - iconKey: 'email-icon2', - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. -[Learn more](${DOCS_URL}/guides/auth/social-login/auth-apple#configure-your-services-id)`, + emailTemplateType: 'authentication', }, } const REAUTHENTICATION: FormSchema = { @@ -210,12 +198,229 @@ const REAUTHENTICATION: FormSchema = { }, }, validationSchema: object().shape({ - MAILER_SUBJECTS_REAUTHENTICATION: string().required('"Subject heading is required.'), + MAILER_SUBJECTS_REAUTHENTICATION: string().required('Subject heading is required.'), + }), + misc: { + emailTemplateType: 'authentication', + }, +} + +// Notifications +const PASSWORD_CHANGED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'PASSWORD_CHANGED_NOTIFICATION', + type: 'object', + title: 'Password changed notification', + purpose: 'Notify a user when their password has been changed', + properties: { + MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_PASSWORD_CHANGED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's email address +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION: string().required( + 'Subject heading is required.' + ), + }), + misc: { + emailTemplateType: 'security', + }, +} + +const EMAIL_CHANGED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'EMAIL_CHANGED_NOTIFICATION', + type: 'object', + title: 'Email changed notification', + purpose: 'Notify a user when their email address has been changed', + properties: { + MAILER_SUBJECTS_EMAIL_CHANGED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_EMAIL_CHANGED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's new email address +- \`{{ .OldEmail }}\` : The user's old email address +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_EMAIL_CHANGED_NOTIFICATION: string().required('Subject heading is required.'), + }), + misc: { + emailTemplateType: 'security', + }, +} + +const PHONE_CHANGED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'PHONE_CHANGED_NOTIFICATION', + type: 'object', + title: 'Phone changed notification', + purpose: 'Notify a user when the phone number has been changed', + properties: { + MAILER_SUBJECTS_PHONE_CHANGED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_PHONE_CHANGED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's email address +- \`{{ .Phone }}\` : The user's new phone number +- \`{{ .OldPhone }}\` : The user's old phone number +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_PHONE_CHANGED_NOTIFICATION: string().required('Subject heading is required.'), + }), + misc: { + emailTemplateType: 'security', + }, +} + +const IDENTITY_LINKED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'IDENTITY_LINKED_NOTIFICATION', + type: 'object', + title: 'Identity linked notification', + purpose: 'Notify a user when a new identity has been linked to their account', + properties: { + MAILER_SUBJECTS_IDENTITY_LINKED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_IDENTITY_LINKED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's email address +- \`{{ .Provider }}\` : The provider of the newly linked identity +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_IDENTITY_LINKED_NOTIFICATION: string().required('Subject heading is required.'), + }), + misc: { + emailTemplateType: 'security', + }, +} + +const IDENTITY_UNLINKED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'IDENTITY_UNLINKED_NOTIFICATION', + type: 'object', + title: 'Identity unlinked notification', + purpose: 'Notify a user when an identity has been unlinked from their account', + properties: { + MAILER_SUBJECTS_IDENTITY_UNLINKED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_IDENTITY_UNLINKED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's email address +- \`{{ .Provider }}\` : The provider of the unlinked identity +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_IDENTITY_UNLINKED_NOTIFICATION: string().required( + 'Subject heading is required.' + ), + }), + misc: { + emailTemplateType: 'security', + }, +} + +const MFA_FACTOR_ENROLLED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'MFA_FACTOR_ENROLLED_NOTIFICATION', + type: 'object', + title: 'MFA factor enrolled notification', + purpose: 'Notify a user when a new MFA factor has been enrolled for their account', + properties: { + MAILER_SUBJECTS_MFA_FACTOR_ENROLLED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_MFA_FACTOR_ENROLLED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's email address +- \`{{ .FactorType }}\` : The type of the newly enrolled MFA factor +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_MFA_FACTOR_ENROLLED_NOTIFICATION: string().required( + 'Subject heading is required.' + ), + }), + misc: { + emailTemplateType: 'security', + }, +} + +const MFA_FACTOR_UNENROLLED_NOTIFICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'MFA_FACTOR_UNENROLLED_NOTIFICATION', + type: 'object', + title: 'MFA factor unenrolled notification', + purpose: 'Notify a user when an MFA factor has been unenrolled from their account', + properties: { + MAILER_SUBJECTS_MFA_FACTOR_UNENROLLED_NOTIFICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_MFA_FACTOR_UNENROLLED_NOTIFICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Email }}\` : The user's email address +- \`{{ .FactorType }}\` : The type of the newly enrolled MFA factor +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_MFA_FACTOR_UNENROLLED_NOTIFICATION: string().required( + 'Subject heading is required.' + ), }), misc: { - iconKey: 'email-icon2', - helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. -[Learn more](${DOCS_URL}/guides/auth/social-login/auth-apple#configure-your-services-id)`, + emailTemplateType: 'security', }, } @@ -226,4 +431,12 @@ export const TEMPLATES_SCHEMAS = [ EMAIL_CHANGE, RECOVERY, REAUTHENTICATION, + // Notifications + PASSWORD_CHANGED_NOTIFICATION, + EMAIL_CHANGED_NOTIFICATION, + PHONE_CHANGED_NOTIFICATION, + IDENTITY_LINKED_NOTIFICATION, + IDENTITY_UNLINKED_NOTIFICATION, + MFA_FACTOR_ENROLLED_NOTIFICATION, + MFA_FACTOR_UNENROLLED_NOTIFICATION, ] diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index 1f5c9921a48bd..26ddf458cbb89 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx @@ -1,14 +1,29 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ChevronRight } from 'lucide-react' +import Link from 'next/link' +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { z } from 'zod' + import { useParams } from 'common' import { useIsSecurityNotificationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { ScaffoldSection } from 'components/layouts/Scaffold' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { ChevronRight } from 'lucide-react' -import Link from 'next/link' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' +import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { + Button, Card, CardContent, + CardFooter, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Switch, Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, @@ -17,11 +32,32 @@ import { import { TEMPLATES_SCHEMAS } from '../AuthTemplatesValidation' import EmailRateLimitsAlert from '../EmailRateLimitsAlert' import { slugifyTitle } from './EmailTemplates.utils' -import TemplateEditor from './TemplateEditor' +import { TemplateEditor } from './TemplateEditor' + +const notificationEnabledKeys = TEMPLATES_SCHEMAS.filter( + (t) => t.misc?.emailTemplateType === 'security' +).map((template) => { + return `MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED` +}) + +const NotificationsFormSchema = z.object({ + ...notificationEnabledKeys.reduce( + (acc, key) => { + acc[key] = z.boolean() + return acc + }, + {} as Record + ), +}) export const EmailTemplates = () => { - const isSecurityNotificationsEnabled = useIsSecurityNotificationsEnabled() const { ref: projectRef } = useParams() + const isSecurityNotificationsEnabled = useIsSecurityNotificationsEnabled() + const { can: canUpdateConfig } = useAsyncCheckPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) + const { data: authConfig, error: authConfigError, @@ -30,11 +66,45 @@ export const EmailTemplates = () => { isSuccess, } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({ + onError: (error) => { + toast.error(`Failed to update settings: ${error?.message}`) + }, + onSuccess: () => { + toast.success('Successfully updated settings') + }, + }) + const builtInSMTP = isSuccess && authConfig && (!authConfig.SMTP_HOST || !authConfig.SMTP_USER || !authConfig.SMTP_PASS) + const defaultValues = notificationEnabledKeys.reduce( + (acc, key) => { + acc[key] = authConfig ? Boolean(authConfig[key as keyof typeof authConfig]) : false + return acc + }, + {} as Record + ) + + const notificationsForm = useForm>({ + resolver: zodResolver(NotificationsFormSchema), + defaultValues, + }) + + const onSubmit = (values: any) => { + if (!projectRef) return console.error('Project ref is required') + updateAuthConfig({ projectRef: projectRef, config: { ...values } }) + } + + useEffect(() => { + if (authConfig) { + notificationsForm.reset(defaultValues) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [authConfig]) + return ( {isError && ( @@ -52,51 +122,145 @@ export const EmailTemplates = () => { {isSuccess && (
{builtInSMTP ? ( -
+
) : null} {isSecurityNotificationsEnabled ? ( - - {TEMPLATES_SCHEMAS.map((template) => { - const templateSlug = slugifyTitle(template.title) - return ( - - -
-

{template.title}

- {template.purpose && ( -

{template.purpose}

+
+
+ Authentication + + {TEMPLATES_SCHEMAS.filter( + (t) => t.misc?.emailTemplateType === 'authentication' + ).map((template) => { + const templateSlug = slugifyTitle(template.title) + + return ( + + +
+

{template.title}

+ {template.purpose && ( +

{template.purpose}

+ )} +
+ +
+ +
+ +
+ ) + })} +
+
+ +
+ Security + +
+ + {TEMPLATES_SCHEMAS.filter( + (t) => t.misc?.emailTemplateType === 'security' + ).map((template) => { + const templateSlug = slugifyTitle(template.title) + const templateEnabledKey = + `MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED` as keyof typeof authConfig + + return ( + + +

{template.title}

+ {template.purpose && ( +

+ {template.purpose} +

+ )} + + +
+ ( + + + + )} + /> + + + + +
+
+ ) + })} + + {notificationsForm.formState.isDirty && ( + )} -
- - - - ) - })} - + + + + + +
+
) : ( - {TEMPLATES_SCHEMAS.map((template) => { - return ( - - {template.title} - - ) - })} + {TEMPLATES_SCHEMAS.filter( + (t) => t.misc?.emailTemplateType === 'authentication' + ).map((template) => ( + + {template.title} + + ))} - {TEMPLATES_SCHEMAS.map((template) => { + {TEMPLATES_SCHEMAS.filter( + (t) => t.misc?.emailTemplateType === 'authentication' + ).map((template) => { const panelId = slugifyTitle(template.title) return ( diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx index 06d3619bc9f70..89d69e61ed8f1 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx @@ -1,6 +1,7 @@ +import { Check, MailWarning } from 'lucide-react' + import { Markdown } from 'components/interfaces/Markdown' import { ValidateSpamResponse } from 'data/auth/validate-spam-mutation' -import { Check, MailWarning } from 'lucide-react' import { Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' interface SpamValidationProps { diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index e70e547934be4..47334435239c0 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -38,7 +38,7 @@ interface TemplateEditorProps { template: FormSchema } -const TemplateEditor = ({ template }: TemplateEditorProps) => { +export const TemplateEditor = ({ template }: TemplateEditorProps) => { const { ref: projectRef } = useParams() const { can: canUpdateConfig } = useAsyncCheckPermissions( PermissionAction.UPDATE, @@ -383,5 +383,3 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { ) } - -export default TemplateEditor diff --git a/apps/studio/pages/project/[ref]/auth/templates/[templateId].tsx b/apps/studio/pages/project/[ref]/auth/templates/[templateId].tsx index f3097485c6b12..30951b5af5b7a 100644 --- a/apps/studio/pages/project/[ref]/auth/templates/[templateId].tsx +++ b/apps/studio/pages/project/[ref]/auth/templates/[templateId].tsx @@ -1,9 +1,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect } from 'react' import { useIsSecurityNotificationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { TEMPLATES_SCHEMAS } from 'components/interfaces/Auth/AuthTemplatesValidation' import { slugifyTitle } from 'components/interfaces/Auth/EmailTemplates/EmailTemplates.utils' -import TemplateEditor from 'components/interfaces/Auth/EmailTemplates/TemplateEditor' +import { TemplateEditor } from 'components/interfaces/Auth/EmailTemplates/TemplateEditor' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' @@ -12,9 +15,6 @@ import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { DOCS_URL } from 'lib/constants' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect } from 'react' import type { NextPageWithLayout } from 'types' import { Button, Card } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' @@ -33,6 +33,16 @@ const RedirectToTemplates = () => { 'custom_config_gotrue' ) + // Find template whose slug matches the URL slug + const template = + templateId && typeof templateId === 'string' + ? TEMPLATES_SCHEMAS.find((template) => slugifyTitle(template.title) === templateId) + : null + + // Convert templateId slug to one lowercase word to match docs anchor tag + const templateIdForDocs = + typeof templateId === 'string' ? templateId.replace(/-/g, '').toLowerCase() : '' + useEffect(() => { if (isPermissionsLoaded && !isSecurityNotificationsEnabled) { router.replace(`/project/${ref}/auth/templates/`) @@ -43,18 +53,12 @@ const RedirectToTemplates = () => { return } - if (!isSecurityNotificationsEnabled) { + if (!isSecurityNotificationsEnabled || !templateId) { return null } - // Find template whose slug matches the URL slug - const template = - templateId && typeof templateId === 'string' - ? TEMPLATES_SCHEMAS.find((template) => slugifyTitle(template.title) === templateId) - : null - // Show error if templateId is invalid or template is not found - if (!template || !templateId || typeof templateId !== 'string') { + if (!template) { return (
{ ) } - // Convert templateId slug to one lowercase word to match docs anchor tag - const templateIdForDocs = templateId.replace(/-/g, '').toLowerCase() - return ( Date: Thu, 30 Oct 2025 17:43:02 +1000 Subject: [PATCH 3/9] Advisor sidebar manager (#39889) * sidebar-manager * storage keys * tests * more ai spots * test fix * revert to default * remove ref * Update apps/studio/state/sidebar-manager-state.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix ts * fix * fux * fux query param * clean * fix * more * mock local storage * simplify * remove provider test * remve useopensidebar * fix(new homepage): open ai assistant on advisor card button clicks * Update apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/studio/state/sidebar-manager-state.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * refine * editor sidebar manager * reset results * advisor sidebar manager * empty state and notice * event tracking * remove variable * remove use effect * open in sidebar * use sidebar old home * Update apps/studio/components/ui/EditorPanel/EditorPanel.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * connect hotkey * Update apps/studio/components/layouts/AppLayout/AssistantButton.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/studio/state/advisor-state.ts Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/studio/state/advisor-state.ts Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * fix * initial prompt * fix(inline editor button): only show keyboard shortcut if hotkey active * cleanup(advisor panel): minor code cleanup * fix(advisor panel): misplaced key on list * fix(advisor panel): add error state * fix(advisor panel): improve a11y * fix(advisor panel): cannot find selected item * fix * fix * tooltip * link * sidebar move up * LayoutSidebarProvider to only sendEvent if in a project --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> Co-authored-by: Joshen Lim --- .../interfaces/Home/AdvisorWidget.tsx | 35 +- .../interfaces/HomeNew/AdvisorSection.tsx | 58 +-- .../interfaces/Linter/Linter.utils.tsx | 2 +- .../layouts/AdvisorsLayout/AdvisorsLayout.tsx | 12 +- .../AdvisorsLayout/AdvisorsSidebarMenu.tsx | 40 +++ .../layouts/AppLayout/AdvisorButton.tsx | 54 +++ .../layouts/AppLayout/AssistantButton.tsx | 21 +- .../layouts/AppLayout/InlineEditorButton.tsx | 41 ++- .../components/layouts/DefaultLayout.tsx | 47 +-- .../FeedbackDropdown/FeedbackDropdown.tsx | 4 +- .../LayoutHeader/HelpPopover.tsx | 36 +- .../LayoutHeader/LayoutHeader.tsx | 28 +- .../NotificationsPopover.tsx | 17 +- .../LayoutSidebar/LayoutSidebarProvider.tsx | 33 +- .../layouts/ProjectLayout/ProjectLayout.tsx | 5 +- .../ui/AIAssistantPanel/AIAssistant.tsx | 7 +- .../ui/AdvisorPanel/AdvisorPanel.tsx | 335 ++++++++++++++++++ .../ui/AdvisorPanel/EmptyAdvisor.tsx | 56 +++ .../components/ui/EditorPanel/EditorPanel.tsx | 2 +- apps/studio/components/ui/FilterPopover.tsx | 2 +- apps/studio/state/advisor-state.ts | 40 +++ apps/studio/state/sidebar-manager-state.tsx | 10 +- packages/common/telemetry-constants.ts | 19 + 23 files changed, 733 insertions(+), 171 deletions(-) create mode 100644 apps/studio/components/layouts/AdvisorsLayout/AdvisorsSidebarMenu.tsx create mode 100644 apps/studio/components/layouts/AppLayout/AdvisorButton.tsx create mode 100644 apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx create mode 100644 apps/studio/components/ui/AdvisorPanel/EmptyAdvisor.tsx create mode 100644 apps/studio/state/advisor-state.ts diff --git a/apps/studio/components/interfaces/Home/AdvisorWidget.tsx b/apps/studio/components/interfaces/Home/AdvisorWidget.tsx index 9a7555a3249b2..3e946b5802fcf 100644 --- a/apps/studio/components/interfaces/Home/AdvisorWidget.tsx +++ b/apps/studio/components/interfaces/Home/AdvisorWidget.tsx @@ -1,14 +1,17 @@ import { Activity, ExternalLink, Shield } from 'lucide-react' import Link from 'next/link' -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useParams } from 'common' import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' -import { EntityTypeIcon, lintInfoMap } from 'components/interfaces/Linter/Linter.utils' +import { EntityTypeIcon, createLintSummaryPrompt } from 'components/interfaces/Linter/Linter.utils' import { useQueryPerformanceQuery } from 'components/interfaces/Reports/Reports.queries' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Lint, useProjectLintsQuery } from 'data/lint/lint-query' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { useAdvisorStateSnapshot } from 'state/advisor-state' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { AiIconAnimation, Card, @@ -27,8 +30,6 @@ import { TabsTrigger_Shadcn_ as TabsTrigger, } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' -import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' interface SlowQuery { rolname: string @@ -48,6 +49,7 @@ export const AdvisorWidget = () => { ) const snap = useAiAssistantStateSnapshot() const { openSidebar } = useSidebarManagerSnapshot() + const { setSelectedItemId } = useAdvisorStateSnapshot() const securityLints = useMemo( () => (lints ?? []).filter((lint: Lint) => lint.categories.includes('SECURITY')), @@ -76,6 +78,14 @@ export const AdvisorWidget = () => { [slowestQueriesData] ) + const handleLintClick = useCallback( + (lint: Lint) => { + setSelectedItemId(lint.cache_key) + openSidebar(SIDEBAR_KEYS.ADVISOR_PANEL) + }, + [setSelectedItemId, openSidebar] + ) + const totalIssues = securityErrorCount + securityWarningCount + performanceErrorCount + performanceWarningCount const hasErrors = securityErrorCount > 0 || performanceErrorCount > 0 @@ -134,15 +144,15 @@ export const AdvisorWidget = () => { className="text-sm w-full border-b my-0 last:border-b-0 group px-4 " >
- handleLintClick(lint)} + className="flex items-center gap-2 transition truncate flex-1 min-w-0 py-3 text-left" >

{lintText.replace(/\\`/g, '`')}

- + { openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) snap.newChat({ name: 'Summarize lint', - initialInput: `Summarize the issue and suggest fixes for the following lint item: - Title: ${lintInfoMap.find((item) => item.name === lint.name)?.title ?? lint.title} - Entity: ${(lint.metadata && (lint.metadata.entity || (lint.metadata.schema && lint.metadata.name && `${lint.metadata.schema}.${lint.metadata.name}`))) ?? 'N/A'} - Schema: ${lint.metadata?.schema ?? 'N/A'} - Issue Details: ${lint.detail ? lint.detail.replace(/\`/g, '`') : 'N/A'} - Description: ${lint.description ? lint.description.replace(/\`/g, '`') : 'N/A'}`, + initialInput: createLintSummaryPrompt(lint), }) }} tooltip={{ - content: { side: 'bottom', text: 'What is this issue?' }, + content: { side: 'bottom', text: 'Help me fix this issue' }, }} />
diff --git a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx b/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx index 492e484a6618c..3bccc7f130b0f 100644 --- a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx @@ -1,34 +1,18 @@ import { BarChart, Shield } from 'lucide-react' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useParams } from 'common' -import LintDetail from 'components/interfaces/Linter/LintDetail' import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' -import { - createLintSummaryPrompt, - LintCategoryBadge, - lintInfoMap, -} from 'components/interfaces/Linter/Linter.utils' +import { createLintSummaryPrompt } from 'components/interfaces/Linter/Linter.utils' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Lint, useProjectLintsQuery } from 'data/lint/lint-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useAdvisorStateSnapshot } from 'state/advisor-state' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' -import { - AiIconAnimation, - Button, - Card, - CardContent, - CardHeader, - CardTitle, - Sheet, - SheetContent, - SheetHeader, - SheetSection, - SheetTitle, -} from 'ui' +import { AiIconAnimation, Button, Card, CardContent, CardHeader, CardTitle } from 'ui' import { Row } from 'ui-patterns' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' @@ -46,8 +30,7 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo const { mutate: sendEvent } = useSendEventMutation() const { data: organization } = useSelectedOrganizationQuery() const { openSidebar } = useSidebarManagerSnapshot() - - const [selectedLint, setSelectedLint] = useState(null) + const { setSelectedItemId } = useAdvisorStateSnapshot() const errorLints: Lint[] = useMemo(() => { return lints?.filter((lint) => lint.level === LINTER_LEVELS.ERROR) ?? [] @@ -84,7 +67,8 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo const handleCardClick = useCallback( (lint: Lint) => { - setSelectedLint(lint) + setSelectedItemId(lint.cache_key) + openSidebar(SIDEBAR_KEYS.ADVISOR_PANEL) if (projectRef && organization?.slug) { sendEvent({ action: 'home_advisor_issue_card_clicked', @@ -100,7 +84,7 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo }) } }, - [sendEvent, projectRef, organization] + [sendEvent, setSelectedItemId, openSidebar, projectRef, organization, totalErrors] ) if (showEmptyState) { @@ -185,32 +169,6 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo ) })} - setSelectedLint(null)}> - - {selectedLint && ( - <> - -
- - {lintInfoMap.find((item) => item.name === selectedLint.name)?.title ?? - 'Unknown'} - - -
-
- - {selectedLint && projectRef && ( - setSelectedLint(null)} - /> - )} - - - )} -
-
) : ( diff --git a/apps/studio/components/interfaces/Linter/Linter.utils.tsx b/apps/studio/components/interfaces/Linter/Linter.utils.tsx index 84d512d441ee4..5f9b61f65786b 100644 --- a/apps/studio/components/interfaces/Linter/Linter.utils.tsx +++ b/apps/studio/components/interfaces/Linter/Linter.utils.tsx @@ -312,7 +312,7 @@ export const LintCTA = ({ return ( diff --git a/apps/studio/components/layouts/AdvisorsLayout/AdvisorsLayout.tsx b/apps/studio/components/layouts/AdvisorsLayout/AdvisorsLayout.tsx index 14635287b9335..3daa22f996d06 100644 --- a/apps/studio/components/layouts/AdvisorsLayout/AdvisorsLayout.tsx +++ b/apps/studio/components/layouts/AdvisorsLayout/AdvisorsLayout.tsx @@ -1,21 +1,15 @@ import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' -import { useIsAdvisorRulesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { ProductMenu } from 'components/ui/ProductMenu' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { ProjectLayout } from '../ProjectLayout/ProjectLayout' -import { generateAdvisorsMenu } from './AdvisorsMenu.utils' +import { AdvisorsSidebarMenu } from './AdvisorsSidebarMenu' export interface AdvisorsLayoutProps { title?: string } const AdvisorsLayout = ({ children }: PropsWithChildren) => { - const { data: project } = useSelectedProjectQuery() - const advisorRules = useIsAdvisorRulesEnabled() - const router = useRouter() const page = router.pathname.split('/')[4] @@ -23,9 +17,7 @@ const AdvisorsLayout = ({ children }: PropsWithChildren) => - } + productMenu={} > {children} diff --git a/apps/studio/components/layouts/AdvisorsLayout/AdvisorsSidebarMenu.tsx b/apps/studio/components/layouts/AdvisorsLayout/AdvisorsSidebarMenu.tsx new file mode 100644 index 0000000000000..06e6395b4ae8a --- /dev/null +++ b/apps/studio/components/layouts/AdvisorsLayout/AdvisorsSidebarMenu.tsx @@ -0,0 +1,40 @@ +import { ProductMenu } from 'components/ui/ProductMenu' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { Badge, Button } from 'ui' +import { FeaturePreviewSidebarPanel } from '../../ui/FeaturePreviewSidebarPanel' +import { generateAdvisorsMenu } from './AdvisorsMenu.utils' +import { useIsAdvisorRulesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' + +interface AdvisorsSidebarMenuProps { + page?: string +} + +export function AdvisorsSidebarMenu({ page }: AdvisorsSidebarMenuProps) { + const { data: project } = useSelectedProjectQuery() + const advisorRules = useIsAdvisorRulesEnabled() + const { toggleSidebar } = useSidebarManagerSnapshot() + + const handleOpenAdvisor = () => { + toggleSidebar(SIDEBAR_KEYS.ADVISOR_PANEL) + } + + return ( +
+ New Location} + actions={ + + } + /> + + +
+ ) +} diff --git a/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx b/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx new file mode 100644 index 0000000000000..6b3c3c3fccc91 --- /dev/null +++ b/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx @@ -0,0 +1,54 @@ +import { Lightbulb } from 'lucide-react' + +import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useProjectLintsQuery } from 'data/lint/lint-query' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { cn } from 'ui' + +export const AdvisorButton = () => { + const { ref: projectRef } = useParams() + const { toggleSidebar, activeSidebar } = useSidebarManagerSnapshot() + const { data: lints } = useProjectLintsQuery({ projectRef }) + + const hasCriticalIssues = Array.isArray(lints) && lints.some((lint) => lint.level === 'ERROR') + + const isOpen = activeSidebar?.id === SIDEBAR_KEYS.ADVISOR_PANEL + + const handleClick = () => { + toggleSidebar(SIDEBAR_KEYS.ADVISOR_PANEL) + } + + return ( +
+ + + + {hasCriticalIssues && ( + + )} +
+ ) +} diff --git a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx index d9e8b835dd9fb..8f550e237864b 100644 --- a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx +++ b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx @@ -2,23 +2,28 @@ import { LOCAL_STORAGE_KEYS } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { AiIconAnimation, KeyboardShortcut } from 'ui' -import { SIDEBAR_KEYS } from '../ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { AiIconAnimation, cn, KeyboardShortcut } from 'ui' export const AssistantButton = () => { - const { toggleSidebar } = useSidebarManagerSnapshot() + const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot() const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT), true ) + const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT + return ( { toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT) }} @@ -33,7 +38,11 @@ export const AssistantButton = () => { }, }} > - + ) } diff --git a/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx b/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx index 57c39df7a54c6..db0a6d6bffa2d 100644 --- a/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx +++ b/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx @@ -1,27 +1,44 @@ +import { LOCAL_STORAGE_KEYS } from 'common' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { SqlEditor } from 'icons' -import { KeyboardShortcut } from 'ui' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { cn, KeyboardShortcut } from 'ui' + +const InlineEditorKeyboardTooltip = () => { + const [hotkeyEnabled] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL), + true + ) + + return hotkeyEnabled ? : null +} + +export const InlineEditorButton = () => { + const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot() + const isOpen = activeSidebar?.id === SIDEBAR_KEYS.EDITOR_PANEL + + const handleClick = () => { + toggleSidebar(SIDEBAR_KEYS.EDITOR_PANEL) + } -export const InlineEditorButton = ({ - onClick, - showShortcut = true, -}: { - onClick: () => void - showShortcut?: boolean -}) => { return ( SQL Editor - {showShortcut && } +
), }, diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index b72a61f215255..8763b5e1b00a1 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -12,6 +12,7 @@ import { SidebarProvider } from 'ui' import { LayoutHeader } from './ProjectLayout/LayoutHeader' import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar' import { ProjectContextProvider } from './ProjectLayout/ProjectContext' +import { LayoutSidebarProvider } from './ProjectLayout/LayoutSidebar/LayoutSidebarProvider' export interface DefaultLayoutProps { headerTitle?: string @@ -55,29 +56,31 @@ const DefaultLayout = ({ return ( - -
- {/* Top Banner */} - -
- - + + +
+ {/* Top Banner */} + +
+ + +
+ {/* Main Content Area */} +
+ {/* Sidebar - Only show for project pages, not account pages */} + {!router.pathname.startsWith('/account') && } + {/* Main Content */} +
{children}
+
- {/* Main Content Area */} -
- {/* Sidebar - Only show for project pages, not account pages */} - {!router.pathname.startsWith('/account') && } - {/* Main Content */} -
{children}
-
-
- + + ) diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx index c73d79a056201..824965c256c33 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx @@ -25,8 +25,8 @@ export const FeedbackDropdown = ({ className }: { className?: string }) => { setIsOpen((isOpen) => !isOpen) setStage('select') }} - type="outline" - className="rounded-full h-[32px] border-border" + type="text" + className="rounded-full h-[32px] text-foreground-light hover:text-foreground" > Feedback diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPopover.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPopover.tsx index 02b69a0b7a18f..3b8c00a168705 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPopover.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPopover.tsx @@ -1,6 +1,7 @@ import { Activity, BookOpen, HelpCircle, Mail, Wrench } from 'lucide-react' import Image from 'next/legacy/image' import { useRouter } from 'next/router' +import { useState } from 'react' import SVG from 'react-inlinesvg' import { IS_PLATFORM } from 'common' @@ -17,12 +18,13 @@ import { Button, ButtonGroup, ButtonGroupItem, + cn, Popover, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, } from 'ui' -import { SIDEBAR_KEYS } from '../LayoutSidebar/LayoutSidebarProvider' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' export const HelpPopover = () => { const router = useRouter() @@ -30,33 +32,39 @@ export const HelpPopover = () => { const { data: org } = useSelectedOrganizationQuery() const snap = useAiAssistantStateSnapshot() const { openSidebar } = useSidebarManagerSnapshot() - const { mutate: sendEvent } = useSendEventMutation() + const [isOpen, setIsOpen] = useState(false) const projectRef = project?.parent_project_ref ?? (router.query.ref as string | undefined) return ( - + - } - tooltip={{ content: { side: 'bottom', text: 'Help' } }} + type="outline" + size="tiny" + className={cn( + 'rounded-full w-[32px] h-[32px] flex items-center justify-center p-0 group', + isOpen && 'bg-foreground text-background' + )} onClick={() => { sendEvent({ action: 'help_button_clicked', groups: { project: project?.ref, organization: org?.slug }, }) }} - /> + tooltip={{ content: { side: 'bottom', text: 'Help' } }} + > + +
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index b7128895db8fe..f4142e8ae02d9 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -3,7 +3,7 @@ import { ChevronLeft } from 'lucide-react' import Link from 'next/link' import { ReactNode, useMemo } from 'react' -import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useParams } from 'common' import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { Connect } from 'components/interfaces/Connect/Connect' import { LocalDropdown } from 'components/interfaces/LocalDropdown' @@ -15,15 +15,12 @@ import { OrganizationDropdown } from 'components/layouts/AppLayout/OrganizationD import { ProjectDropdown } from 'components/layouts/AppLayout/ProjectDropdown' import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils' import { useOrgUsageQuery } from 'data/usage/org-usage-query' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM } from 'lib/constants' import { useRouter } from 'next/router' import { useAppStateSnapshot } from 'state/app-state' -import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { Badge, cn } from 'ui' -import { SIDEBAR_KEYS } from '../LayoutSidebar/LayoutSidebarProvider' import { BreadcrumbsView } from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown' import { HelpPopover } from './HelpPopover' @@ -31,6 +28,8 @@ import { HomeIcon } from './HomeIcon' import { LocalVersionPopover } from './LocalVersionPopover' import MergeRequestButton from './MergeRequestButton' import { NotificationsPopoverV2 } from './NotificationsPopoverV2/NotificationsPopover' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { AdvisorButton } from 'components/layouts/AppLayout/AdvisorButton' const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps) => ( @@ -71,13 +70,8 @@ const LayoutHeader = ({ const { data: selectedOrganization } = useSelectedOrganizationQuery() const { setMobileMenuOpen } = useAppStateSnapshot() const gitlessBranching = useIsBranching2Enabled() - const { toggleSidebar } = useSidebarManagerSnapshot() const isAccountPage = router.pathname.startsWith('/account') - const [inlineEditorHotkeyEnabled] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL), - true - ) // We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap) const { data: orgUsage } = useOrgUsageQuery( @@ -216,16 +210,14 @@ const LayoutHeader = ({ <> -
+
{!!projectRef && ( <> - toggleSidebar(SIDEBAR_KEYS.EDITOR_PANEL)} - showShortcut={inlineEditorHotkeyEnabled} - /> + + )} @@ -236,14 +228,12 @@ const LayoutHeader = ({ ) : ( <> -
+
{!!projectRef && ( <> - toggleSidebar(SIDEBAR_KEYS.EDITOR_PANEL)} - showShortcut={inlineEditorHotkeyEnabled} - /> + + )} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopoverV2/NotificationsPopover.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopoverV2/NotificationsPopover.tsx index bf3b1dd16a3e3..17cfdcb8d34c5 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopoverV2/NotificationsPopover.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopoverV2/NotificationsPopover.tsx @@ -99,29 +99,34 @@ export const NotificationsPopoverV2 = () => { text: 'Notifications', }, }} - type="text" - className={cn('rounded-none h-[30px] w-[32px] group relative')} + type="outline" + size="tiny" + className={cn( + 'rounded-full w-[32px] h-[32px] flex items-center justify-center p-0 group', + open && 'bg-foreground text-background' + )} icon={
{hasCritical && ( -
+
)} {hasWarning && !hasCritical && ( -
+
)} {!!hasNewNotifications && !hasCritical && !hasWarning && ( -
+
)} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx index bc946ffbabe76..85b8d86d5ea0b 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx @@ -1,20 +1,49 @@ import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' -import { useRegisterSidebar, useSidebarManagerSnapshot } from 'state/sidebar-manager-state' + +import { AdvisorPanel } from 'components/ui/AdvisorPanel/AdvisorPanel' import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant' import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useRegisterSidebar, useSidebarManagerSnapshot } from 'state/sidebar-manager-state' export const SIDEBAR_KEYS = { AI_ASSISTANT: 'ai-assistant', EDITOR_PANEL: 'editor-panel', + ADVISOR_PANEL: 'advisor-panel', } as const +// LayoutSidebars are meant to be used within a project, but rendered within DefaultLayout +// to prevent unnecessary registering / unregistering of sidebars with every route change export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => { + const { data: project } = useSelectedProjectQuery() + const { data: org } = useSelectedOrganizationQuery() + const { mutate: sendEvent } = useSendEventMutation() + useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => , {}, 'i') useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => , {}, 'e') + useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => ) const router = useRouter() - const { openSidebar } = useSidebarManagerSnapshot() + const { openSidebar, activeSidebar } = useSidebarManagerSnapshot() + + useEffect(() => { + if (!!project && activeSidebar) { + // add event tracking + sendEvent({ + action: 'sidebar_opened', + properties: { + sidebar: activeSidebar.id as (typeof SIDEBAR_KEYS)[keyof typeof SIDEBAR_KEYS], + }, + groups: { + project: project?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, + }) + } + }, [activeSidebar]) // Handle sidebar URL parameter useEffect(() => { diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 507121413df39..cb259ffca56f6 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -21,7 +21,6 @@ import { useEditorType } from '../editors/EditorsLayout.hooks' import BuildingState from './BuildingState' import ConnectingState from './ConnectingState' import { LayoutSidebar } from './LayoutSidebar' -import { LayoutSidebarProvider } from './LayoutSidebar/LayoutSidebarProvider' import { LoadingState } from './LoadingState' import { ProjectPausedState } from './PausedState/ProjectPausedState' import { PauseFailedState } from './PauseFailedState' @@ -216,9 +215,7 @@ export const ProjectLayout = forwardRef - - - + diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index b14e147105906..4f4bf8a8bfc72 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -61,7 +61,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { const disablePrompts = useFlag('disableAssistantPrompts') const { snippets } = useSqlEditorV2StateSnapshot() const snap = useAiAssistantStateSnapshot() - const { closeSidebar, isSidebarOpen } = useSidebarManagerSnapshot() + const { closeSidebar, activeSidebar } = useSidebarManagerSnapshot() const isPaidPlan = selectedOrganization?.plan?.id !== 'free' @@ -435,11 +435,12 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { }, [snap.initialInput]) useEffect(() => { - if (isSidebarOpen(SIDEBAR_KEYS.AI_ASSISTANT) && isInSQLEditor && !!snippetContent) { + const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT + if (isOpen && isInSQLEditor && !!snippetContent) { snap.setSqlSnippets([{ label: 'Current Query', content: snippetContent }]) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSidebarOpen, isInSQLEditor, snippetContent]) + }, [activeSidebar?.id, isInSQLEditor, snippetContent]) return ( + source: 'lint' + original: Lint +} + +const severityOptions = [ + { label: 'Critical', value: 'critical' }, + { label: 'Warning', value: 'warning' }, + { label: 'Info', value: 'info' }, +] + +const severityOrder: Record = { + critical: 0, + warning: 1, + info: 2, +} + +const severityLabels: Record = { + critical: 'Critical', + warning: 'Warning', + info: 'Info', +} + +const severityBadgeVariants: Record = { + critical: 'destructive', + warning: 'warning', + info: 'default', +} + +const severityColorClasses: Record = { + critical: 'text-destructive', + warning: 'text-warning', + info: 'text-foreground-light', +} + +const tabIconMap: Record, React.ElementType> = { + security: Shield, + performance: Gauge, + messages: Inbox, +} + +const lintLevelToSeverity = (level: Lint['level']): AdvisorSeverity => { + switch (level) { + case 'ERROR': + return 'critical' + case 'WARN': + return 'warning' + default: + return 'info' + } +} + +export const AdvisorPanel = () => { + const { + activeTab, + severityFilters, + selectedItemId, + setActiveTab, + setSeverityFilters, + clearSeverityFilters, + setSelectedItemId, + } = useAdvisorStateSnapshot() + const { data: project } = useSelectedProjectQuery() + const { activeSidebar, closeSidebar } = useSidebarManagerSnapshot() + + const isSidebarOpen = activeSidebar?.id === SIDEBAR_KEYS.ADVISOR_PANEL + + const { + data: lintData, + isLoading: isLintsLoading, + isError: isLintsError, + } = useProjectLintsQuery( + { projectRef: project?.ref }, + { enabled: isSidebarOpen && !!project?.ref } + ) + + const lintItems = useMemo(() => { + if (!lintData) return [] + + return lintData + .map((lint): AdvisorItem | null => { + const categories = lint.categories || [] + const tab = categories.includes('SECURITY') + ? ('security' as const) + : categories.includes('PERFORMANCE') + ? ('performance' as const) + : undefined + + if (!tab) return null + + return { + id: lint.cache_key, + title: lint.detail, + severity: lintLevelToSeverity(lint.level), + createdAt: undefined, + tab, + source: 'lint' as const, + original: lint, + } + }) + .filter((item): item is AdvisorItem => item !== null) + }, [lintData]) + + const combinedItems = useMemo(() => { + const all = [...lintItems] + + return all.sort((a, b) => { + const severityDiff = severityOrder[a.severity] - severityOrder[b.severity] + if (severityDiff !== 0) return severityDiff + + const createdDiff = (b.createdAt ?? 0) - (a.createdAt ?? 0) + if (createdDiff !== 0) return createdDiff + + return a.title.localeCompare(b.title) + }) + }, [lintItems]) + + const filteredItems = useMemo(() => { + return combinedItems.filter((item) => { + if (severityFilters.length > 0 && !severityFilters.includes(item.severity)) { + return false + } + + if (activeTab === 'all') return true + + return item.tab === activeTab + }) + }, [combinedItems, severityFilters, activeTab]) + + const itemsFilteredByTabOnly = useMemo(() => { + return combinedItems.filter((item) => { + if (activeTab === 'all') return true + return item.tab === activeTab + }) + }, [combinedItems, activeTab]) + + const hiddenItemsCount = itemsFilteredByTabOnly.length - filteredItems.length + + const selectedItem = combinedItems.find((item) => item.id === selectedItemId) + const isDetailView = !!selectedItem + + const isLoading = isLintsLoading + const isError = isLintsError + + const handleTabChange = (tab: string) => { + setActiveTab(tab as AdvisorTab) + } + + const handleBackToList = () => { + setSelectedItemId(undefined) + } + + const handleClose = () => { + closeSidebar(SIDEBAR_KEYS.ADVISOR_PANEL) + } + + return ( +
+ {isDetailView ? ( + <> +
+ } + onClick={handleBackToList} + tooltip={{ content: { side: 'bottom', text: 'Back to list' } }} + /> +
+
+ {selectedItem?.title} +
+ {selectedItem && ( + + {severityLabels[selectedItem.severity]} + + )} +
+ } + onClick={handleClose} + tooltip={{ content: { side: 'bottom', text: 'Close Advisor Center' } }} + /> +
+
+ {selectedItem ? ( + + ) : ( +
+

+ Select an advisor item to view more details. +

+
+ )} +
+ + ) : ( + <> +
+
+ + + + All + + + Security + + + Performance + + + +
+ setSeverityFilters(values as AdvisorSeverity[])} + /> + } + onClick={handleClose} + tooltip={{ content: { side: 'bottom', text: 'Close Advisor Center' } }} + /> +
+
+
+
+ {isLoading ? ( +
+ +
+ ) : isError ? ( +
+ +

Error loading advisories

+

Please try again later.

+
+ ) : filteredItems.length === 0 ? ( + 0} + onClearFilters={clearSeverityFilters} + /> + ) : ( + <> +
+ {filteredItems.map((item) => { + const SeverityIcon = tabIconMap[item.tab] + const severityClass = severityColorClasses[item.severity] + return ( +
+ +
+ ) + })} +
+ {severityFilters.length > 0 && hiddenItemsCount > 0 && ( +
+ +
+ )} + + )} +
+ + )} +
+ ) +} + +interface AdvisorDetailProps { + item: AdvisorItem + projectRef: string +} + +const AdvisorDetail = ({ item, projectRef }: AdvisorDetailProps) => { + if (item.source === 'lint') { + const lint = item.original as Lint + return ( +
+ +
+ ) + } +} diff --git a/apps/studio/components/ui/AdvisorPanel/EmptyAdvisor.tsx b/apps/studio/components/ui/AdvisorPanel/EmptyAdvisor.tsx new file mode 100644 index 0000000000000..3096b0b19a9c1 --- /dev/null +++ b/apps/studio/components/ui/AdvisorPanel/EmptyAdvisor.tsx @@ -0,0 +1,56 @@ +import { TextSearch } from 'lucide-react' +import { Button } from 'ui' +import { AdvisorTab } from 'state/advisor-state' + +interface EmptyAdvisorProps { + activeTab: AdvisorTab + hasFilters: boolean + onClearFilters: () => void +} + +export const EmptyAdvisor = ({ activeTab, hasFilters, onClearFilters }: EmptyAdvisorProps) => { + const getHeading = () => { + if (hasFilters) return 'No items found' + + switch (activeTab) { + case 'security': + return 'No security issues detected' + case 'performance': + return 'No performance issues detected' + case 'messages': + return 'No messages' + default: + return 'No issues detected' + } + } + + const getMessage = () => { + if (hasFilters) return 'No advisor items match your current filters' + + switch (activeTab) { + case 'security': + return 'Congrats! There are no security issues detected for this project' + case 'performance': + return 'Congrats! There are no performance issues detected for this project' + case 'messages': + return 'There are no messages for this project' + default: + return 'Congrats! There are no issues detected for this project' + } + } + + return ( +
+ +
+

{getHeading()}

+

{getMessage()}

+
+ {hasFilters && ( + + )} +
+ ) +} diff --git a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx index e18c0b3ebd9de..ea19b47c2da74 100644 --- a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx +++ b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx @@ -147,7 +147,7 @@ export const EditorPanel = () => { return (
-
{label}
+
{label}
{templates.length > 0 && ( diff --git a/apps/studio/components/ui/FilterPopover.tsx b/apps/studio/components/ui/FilterPopover.tsx index e8260e71fafb1..4ae0b2365a5e0 100644 --- a/apps/studio/components/ui/FilterPopover.tsx +++ b/apps/studio/components/ui/FilterPopover.tsx @@ -89,7 +89,7 @@ export const FilterPopover = >({ }) useEffect(() => { - if (!open && activeOptions.length > 0) setSelectedOptions(activeOptions) + if (!open) setSelectedOptions(activeOptions) if (!open) setSearch('') }, [open, activeOptions]) diff --git a/apps/studio/state/advisor-state.ts b/apps/studio/state/advisor-state.ts new file mode 100644 index 0000000000000..a5839330a32ec --- /dev/null +++ b/apps/studio/state/advisor-state.ts @@ -0,0 +1,40 @@ +import { proxy, snapshot, useSnapshot } from 'valtio' + +export type AdvisorTab = 'all' | 'security' | 'performance' | 'messages' +export type AdvisorSeverity = 'critical' | 'warning' | 'info' + +const initialState = { + activeTab: 'all' as AdvisorTab, + severityFilters: ['critical'] as AdvisorSeverity[], + selectedItemId: undefined as string | undefined, +} + +export const advisorState = proxy({ + ...initialState, + setActiveTab(tab: AdvisorTab) { + advisorState.activeTab = tab + }, + setSeverityFilters(severities: AdvisorSeverity[]) { + advisorState.severityFilters = severities + }, + clearSeverityFilters() { + advisorState.severityFilters = [] + }, + setSelectedItemId(id: string | undefined) { + advisorState.selectedItemId = id + }, + focusItem({ id, tab }: { id: string; tab?: AdvisorTab }) { + if (tab) { + advisorState.activeTab = tab + } + advisorState.selectedItemId = id + }, + reset() { + Object.assign(advisorState, initialState) + }, +}) + +export const getAdvisorStateSnapshot = () => snapshot(advisorState) + +export const useAdvisorStateSnapshot = (options?: Parameters[1]) => + useSnapshot(advisorState, options) diff --git a/apps/studio/state/sidebar-manager-state.tsx b/apps/studio/state/sidebar-manager-state.tsx index bb455845ec491..e0e48404090a4 100644 --- a/apps/studio/state/sidebar-manager-state.tsx +++ b/apps/studio/state/sidebar-manager-state.tsx @@ -151,12 +151,16 @@ export const useRegisterSidebar = ( ) useEffect(() => { - const { registerSidebar, unregisterSidebar } = sidebarManagerState + const { registerSidebar, unregisterSidebar, sidebars } = sidebarManagerState - registerSidebar(id, component, handlers) + if (!sidebars[id]) { + registerSidebar(id, component, handlers) + } return () => { - unregisterSidebar(id) + if (sidebars[id]) { + unregisterSidebar(id) + } } }, [id]) diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 9c1e97d46a299..3afd9419f2e89 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2010,6 +2010,24 @@ export interface CommandMenuCommandClickedEvent { groups: Partial } +/** + * User opened a sidebar panel. + * + * @group Events + * @source studio + * @page Various pages with sidebar buttons + */ +export interface SidebarOpenedEvent { + action: 'sidebar_opened' + properties: { + /** + * The sidebar panel that was opened, e.g. ai-assistant, editor-panel, advisor-panel + */ + sidebar: 'ai-assistant' | 'editor-panel' | 'advisor-panel' + } + groups: TelemetryGroups +} + /** * User was exposed to the table quickstart experiment. * @@ -2286,3 +2304,4 @@ export type TelemetryEvent = | CommandMenuOpenedEvent | CommandMenuSearchSubmittedEvent | CommandMenuCommandClickedEvent + | SidebarOpenedEvent From 1fabe2f13aa313cccaba5db28349c6cf3a6d7f4d Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:52:14 +1000 Subject: [PATCH 4/9] chore: weighted tweets (#39917) * add new tweets and remove old * randomise on sign in * prefer higher weights * single source of truth * additional tweet * clean up extraneous content --- .../layouts/SignInLayout/SignInLayout.tsx | 21 +++- .../Sections/TwitterSocialProof.tsx | 7 +- apps/www/data/home/content.tsx | 4 +- apps/www/data/solutions/beginners.tsx | 4 +- .../twitter-profiles/2SQwtv8c_400x400.jpg | Bin 0 -> 33339 bytes .../twitter-profiles/UCBhUBZl_400x400.jpg | Bin 19312 -> 0 bytes .../twitter-profiles/Y1swF6ef_400x400.jpg | Bin 0 -> 22380 bytes .../twitter-profiles/_iAaSUQf_400x400.jpg | Bin 0 -> 24000 bytes .../twitter-profiles/k0aPYRHF_400x400.jpg | Bin 0 -> 33293 bytes .../twitter-profiles/w8HLdlC7_400x400.jpg | Bin 14331 -> 0 bytes packages/shared-data/index.ts | 11 ++- packages/shared-data/tweets.ts | 93 ++++++++++++++---- 12 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 apps/www/public/images/twitter-profiles/2SQwtv8c_400x400.jpg delete mode 100644 apps/www/public/images/twitter-profiles/UCBhUBZl_400x400.jpg create mode 100644 apps/www/public/images/twitter-profiles/Y1swF6ef_400x400.jpg create mode 100644 apps/www/public/images/twitter-profiles/_iAaSUQf_400x400.jpg create mode 100644 apps/www/public/images/twitter-profiles/k0aPYRHF_400x400.jpg delete mode 100644 apps/www/public/images/twitter-profiles/w8HLdlC7_400x400.jpg diff --git a/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx b/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx index 0785b981c5c45..4b0ce98a50ab0 100644 --- a/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx +++ b/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx @@ -86,9 +86,24 @@ const SignInLayout = ({ } | null>(null) useEffect(() => { - const randomQuote = tweets[Math.floor(Math.random() * tweets.length)] - - setQuote(randomQuote) + // Weighted random selection + // Calculate total weight (default weight is fallbackWeight for tweets without weight specified) + const fallbackWeight = 1 + const totalWeight = tweets.reduce((sum, tweet) => sum + (tweet.weight ?? fallbackWeight), 0) + + // Generate random number between 0 and totalWeight + const random = Math.random() * totalWeight + + // Find the selected tweet based on cumulative weights + let accumulatedWeight = 0 + for (const tweet of tweets) { + const weight = tweet.weight ?? fallbackWeight + accumulatedWeight += weight + if (random <= accumulatedWeight) { + setQuote(tweet) + break + } + } }, []) return ( diff --git a/apps/www/components/Sections/TwitterSocialProof.tsx b/apps/www/components/Sections/TwitterSocialProof.tsx index 1c436bdc959fb..615072fe1a0fc 100644 --- a/apps/www/components/Sections/TwitterSocialProof.tsx +++ b/apps/www/components/Sections/TwitterSocialProof.tsx @@ -1,22 +1,23 @@ +import { range } from 'lib/helpers' import Link from 'next/link' import { useRouter } from 'next/router' import { cn } from 'ui' import { TweetCard } from 'ui-patterns/TweetCard' -import { range } from 'lib/helpers' -import tweets from 'shared-data/tweets' import { useBreakpoint } from 'common' import React from 'react' +import { topTweets } from 'shared-data/tweets' interface Props { className?: string } +const tweetsData = topTweets + const TwitterSocialProof: React.FC = ({ className }) => { const { basePath } = useRouter() const isSm = useBreakpoint() const isMd = useBreakpoint(1024) - const tweetsData = tweets.slice(0, 18) return ( <> diff --git a/apps/www/data/home/content.tsx b/apps/www/data/home/content.tsx index fcfc96dd66ea6..0e081811b880f 100644 --- a/apps/www/data/home/content.tsx +++ b/apps/www/data/home/content.tsx @@ -5,7 +5,7 @@ import { Button } from 'ui' import ProductModules from '../ProductModules' import MainProducts from 'data/MainProducts' -import tweets from 'shared-data/tweets' +import { topTweets } from 'shared-data/tweets' import { IconDiscord } from 'ui' export default () => { @@ -188,7 +188,7 @@ export default () => { ), - tweets: tweets.slice(0, 18), + tweets: topTweets, }, } } diff --git a/apps/www/data/solutions/beginners.tsx b/apps/www/data/solutions/beginners.tsx index e217af26e7255..cd9b89ee0304c 100644 --- a/apps/www/data/solutions/beginners.tsx +++ b/apps/www/data/solutions/beginners.tsx @@ -21,7 +21,7 @@ import { import { useBreakpoint } from 'common' import { useSendTelemetryEvent } from 'lib/telemetry' -import { tweets } from 'shared-data' +import { topTweets } from 'shared-data' import { PRODUCT_SHORTNAMES } from 'shared-data/products' const AuthVisual = dynamic(() => import('components/Products/AuthVisual')) @@ -495,7 +495,7 @@ const data: () => { ), - tweets: tweets.slice(0, 18), + tweets: topTweets, }, platformStarterSection: { id: 'platform-starter', diff --git a/apps/www/public/images/twitter-profiles/2SQwtv8c_400x400.jpg b/apps/www/public/images/twitter-profiles/2SQwtv8c_400x400.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8e86078de9d61f5d5b5cd6c35e6a69745de38f83 GIT binary patch literal 33339 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 J7Wr@O{{VC!!TtaM literal 0 HcmV?d00001 diff --git a/apps/www/public/images/twitter-profiles/UCBhUBZl_400x400.jpg b/apps/www/public/images/twitter-profiles/UCBhUBZl_400x400.jpg deleted file mode 100644 index 86a1b942657c4a3c8aaeed650591253a09e52a7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19312 zcmb69by!#KFi}x+igWRN{3IeGLdPy8FDWF)FD&xu zzl*@2qM>18V3GlWWS-GhTR(sl0Dy&q`ER)Y zXE3k;ICum^q<1IryW>CmKiB&w4D7q}Z3Tc12Lpfw!U5mU(8i05R4Rs9qbGoXG6^-K z;Y8t_DAcJ5D3mm?*GM1O&T?@%TrR4Ui@EKr#jpbnYy({qz$KY*Oo0hNvv6A)Irv^8 zGnvq^P*!QM83A=YkzT*SYP?6IWK~e4ouahHpt`-(a#8x3G4h8I3k3}&MsK-U`0zj? zb~r=Z@mMO(pLF>I)JgBZOyxw6Mh%*FCn(o*sIfhT9mH!;lLQbIES%(!g+JV|Jyx^Z zovfE)JWUwMo+y5F|G^THH{z@juYMkFnAwQtOkMevIa--~;t=hm0D`oXS|>$Zcj-G6^> zweEf59c81-g^NW*WF;xYwzE_m2p!T6w3c+5>!s}LP1q)bbpEB&QUSAH!$1$Ml~a8) zDt=#mynnR`HeFx3T~3;`+S$ueXrDFC?yV-z>@Upjd~xTo-ZA#^A@hFpZI0dKpm4&T z-oL&4X5Sd~*A>hy?DFV&%(L-nc1KFBiZd%S*-CrYzmdEl2Q4X__*`dXWcxyu+w?>Y|vDQP$vG(#6 z{?QGBHN!n-rcOuTk|po)WBM2GQxVlg7kBTcnpMMPLXxI6V>FHh3Hl$_%*S|6m&7n} z&qRx;IobzR3MN(ebz*~x5IB!|$3M$X@l&&ABkU1TdOd-ubQ5}wR!952@unus3DdTa zSd`Oi8gECv>$$;)l7-~yBlJgC`-Q0T>qtZTD1!MmQ^JC##_fySYoRmpwT6<*nP zpOk+>i}UN=TMJbieoUSweszaEbqC5!T5TMEZ0C>klb$_1?W^1^X3|PCGq0~&FI#O4 z`IPM2N{I55Omr0otO#_oqC+;W))!Cq?dJPb4I`Q=$;M9pe)jF#J9K@xU?C}AThP{7 zRJ0NOH8hv`Y;aLQQo-G%qgZ|pIdD!Qsa#|>$+DYhQpWgrGTA(%UGEKY=$i{$6S>1G zW$sQTLN;UgapvuxhxA3bV)Vfyy@T7SQ+Xvfe*CU>=Rzgsu{`LfhM5i^%0Tm*4A8HCIHv#%GS|+#BqL|E3;GqvS7Kv)J%I|FwZ;UvM#IgweFguerV#_0gqH! z>rT*dCZpC5(L~30vXeYjCDBkibF}{I;a@bLwv)G02=RK3Z6vELT{sNJ%)VXfix-bY zsrbe5kW^lbH{ z3oC9x!8NDfNQl{C0{;eDZc7yme%)!xMAyOc%<_0h&E)s_FJ&M}a%mo9Z2Wy_`s0V@ z{aM-bcyejQX3nIZhc1>5OQl{s#Uaas>xR1ml4?^GUb&(+Yr=rL@bH@LXf#nR{OBs@ z15x-v%se5`#-&mf`_aX1iBC6Hw`N7bgD#^ul5pA;G0121tXh!1i2X0uGW+id-*sgv z-raf}J?3Nm&$Z4Xy~A_PU{E9iA7+RHx!OOKTqSpDX_si6SY|K*s%PNObey^|=TVhd zFQTRz05ZnDjV5RGTv55#-FCGF->==)NDKL|`dOtFf@5{Rr^L34tuc5kwG)(OQqI4O z`Fd_3*UhD~&2F>(CEz!TSLDv6xPG^4KKh_{>#q$AW+8XoOt9HFFbWS;AC z7HM!7^rf>5@m11S(jf5R;GR9$tSOxQFh&POxa1rM~@xgAJB~BJCngS~Fn)b$Dj)@O-kgx^bg$hu<1` zJcX=wz3EitZ>UT(o}5Rt=a$SHo6&~OO5Zy2WC~1& zPu#ML+~%p0Bvk01=c}}e=e6}KOM+MBnCn0KfqR5GNax&6j=<;F`JTlC&1s#!d>Dt9 zG(|Ba_?6w|#;CdyQHR^Q2zFp7E4bOhtQLZo_Kg??HD~l^IR=(`y3o%920FIwp<|J9?N|mMW#fo ziCQ}LF=aZw_aTv-`l*Aj`|ZZWy_7X3esh906a&Ey4xSQKT5xs94xLRIan5jtGJyyN z09se#&u=TWd1n_)KmaT(3>-Wx>^lSbAByoG^}q(=P{HF;a#3@F#PP%sXt>qh*#`1E z2Z2F=og=Zfd0eJ&viAQu{svGaM+hD|Hg6nXn{!wGhwvL&Fg4nz>DL>eZ?LKduJh^| zi3OqlYk8(I> zO6hs%D~{?ms}K3F@u?d}zoAoHr)`Z-;xo0;Gd**x_>@^t75ca}=&_$!zxTL*L>e$4^$x^w_Dy9p7B-nSarl0yqsoCAautOM&FACR_nWwYY!)0fI(9~OtfNxw~!z&h^1U&3l zY$$>^Mx0xTy>77M#33Iupjk`PpFGI?KA25#4nmZhcuexReCYG~?Q9+=AKFpYfhz+B zV_cJrZ-B2^sn8r-P+!yJxqYius;@ET)(rwBI@mRQ5od2b;G%0joMT`pF!J9e#s%fB zw*kL4ai|ea5WH-JKgZun9n4!jS>AEwV!aXzg}>{fv6|QUJBZL`QRzXWZeaZQ$EYsLeRE)xE9$@43-jR%&yAQY z)iOi-Hvn@V`2o!zzQ7LN-ZTe(g*U(lz_4en8G$8DdV^TY!Y>2A8$M^q!}XutnMLY_ z)3q5)nT+W18zwumZ_VWV#;b;qFQ470+L-f^$X4CZol+dpm%;4-`Xzz%@tfT>WdFDf z^Vn=p`Sk2c21FiBmT5AA2gXmPA_RQ%5XcKYI(Z>IGHSbi5{tRI^qRzORYnHU2ZdAy zY7DvfL)i#Huti@f1>E23kxuN@hV$-S;$w}6U}8;KoYc{y7L|&!N{71jQ7)t=K)90l zJ*Mm+HMf$5LF{(k(+gE zEC6H9cXRTg(>ZGgdv$!$!rFkJV`p^LWDJ&NMSc@uyv8$a^l=fVZ6W!!{yA(_%PR7I zDx~ihmZH^sDug~9NxgH zmUDn)v(#2{V_2f0WH%>!n4`rq7n6372Q_Et*1x!`2p>7?eP zBUC-em}hXshwo1RMv=0+Nct%_d3(I)a$NG4lukDW>FkxLLNZV(at+8AW0mY7)N?9> zFQ6u{f__?%^F2o*v{{5aGr6-s`+eU#Fekw5&8?c;uIkv02yS6YucR?X!-csRmz|d8 zzIil8>Hmtpp@bB;#=XTY!%cSjEvD`YPWoCV+*1N;Boy1qHq4Z{=tpC5s(xw{EVzP) zRPB>x#WxK1=J5By1=pDslv+l{U>}mhWR)CN@=KWS`}gY&N#YG^W>(zC!4AwncDNUP zAmoHi`EVY>O2U-W6&rz&3~_Db?O)}?C1@wyp^DgLeYUnf>U0jCln6NPwTuGOkO<0y73 zKiyW43t1^rWgM-jYXk2bV=efixIpG)(!rKeGm{b$%W*UAOjXE;A^qZf%Jt-|ty}^JOX;&!HKm$`VRozYj*-{Fn_+VC<;|2)XMPOmGBCpSVXE;|&y_ zbqB8JpYmG##+8Wcxl4+gI*}G4IJmg3ZLod9 zu>}hVu7mnF`6qUqV(pxXuH{RAQ{(0PJP^+jn5Fg&Gx9BpQo^oMG1;sv7b%m;VN64$4R)TfhC& zk0CrIIl1-)U8IZJ&(?hA<~^3Y0ZierN}iv6e4UTQ9xn<~;tQsL>68HXEH?RhF>M)4X`3);OP)yY6@0wt;MVu znm$m8C|Y6furqyUb9C7MvAK6L_pT=WKfMSB78~xJ&2dt4;fldii{q*P50iWMi^7~1 zIq=nW7V6OIG$>HWv*27>{!Z2RF*VZ7NwWG<;MJrGihK=UdIM0~rCVLnT83}5amYOp zWW^4z#VnIZ%dY+6=dexXS;&hVLX>trzT1i};@Fj;d5#p7vf+S4oLCR_;6IZKER*5A zq}Mur7GEh;$os6%i#V?S?F|6tN)i!03O=l$i_S%gQVM5yVKf3c=Po4&7Yb|};ZLUT zP|X;C{C=}rST8<{Fo6UPW&Z5S4m0Uf8{g;Q(VHasoPw1J6bql{%QSFVZy6(PXDk>? zcV?OmSbC6a(SmmIe!7uIBCjaBANb3sG7%P%Craa&NMExm)kf+iH2MuM_cSYjp=mLD z#^-w&`1|V^n_8}KfLV{0^C9+((MJnHYEDJ@r*kQ*Jnt$!!mqCcXsHnQQm(ie6C{1Iy~M0?}*U+FCuV&|HrQXN4CCGJ!a%AqG8B>N6OTX#}q#R0o^(bVcF%b3YsJ7aU zbgUCdsB4~)#&1kZ7t+xO-?EK)`gpq84`fEK;{Qv-BJ4n$KX|5@9H~~)_OW6ntH%Bd zDQXR)P~67YAb4SX|IZ|T{z^k#StIFRu2kFm_Vc_WCWQLBd~^cdCz2fqfPsUBM|^*} z|1VJiU{k@q&m<=oHAqZc-P{#UEhrI}hTE*b?f+#fh@!A>0EKV8tCfpfMx$aZLn}r{ zQ_I;u=KV*8mexg@E6$qeq|s`OHZPR5nF3apr5HI5OYDw1Jc!6DyBu8FDpQw>`4z9G zqZ%oOmfPtEp>&UbDp9RKbW4nU0}I`1#Td;Cd9yq5;*co3<>w_k$I6XWle`=m)#Je4XZHo=WQutPsYinRtVp;BC{2rRCf!WA$Ep!uL=gf z)DTTPnV{=f`36@n)!YKdc?M84qVa;M65=z%k3BqRU_13*aNl(~eE}$QKXig`Ir7O@+jJBg6Hm>spLC6gAS70r~xu1Ey(|pCk2x_<{r3 zvtJ8Y?bp-H(ia|$XKr(989ow~;N^@*#I)SkMW*Nox(OtA>b8)JVBOqXzew&@I@xSAZwC}@*poD1nU3TVf z4S6G;)qTwzls4Tbu~|(!?$$lXNw6QGG}lL>e7nr`0CQ*gFo3N>pfd?j3@xg-DKr{? z>`?>b&Tu`4vl#WVgR%3WROogt<}~dayILXksGS<#d`pFzmKaK)TP%xbT@oSk^o_S_ zxn}kO%Qjk|g0&Uz^888RB*7zhN+VHqP6O)wFcsu`<;i%?Z(w-nHP+9&bw1wK>~v#c zE&t!lagJ3tG%1tH*O(cz_8Ahi-&Ab)Z0b?k?k4Yi zlF>(H{hgz05?;-msdLOs7*qev!^-^oke_kgS&44+g$>XCvg41F>kWjK@?IH|S?SPHklV8f2XvbGT79JR`?vWpVbZ$^ozfI4cecw3 z#c-d#=`&p}qX8$K7tu-k&LxrQ6-pbp6V*b&#c4=VTt|SgEBNllqRQW00jab8koUzd z`wftNRZL-Caq!&_@u>W7t)Ct_CwkjR{|jI+ z*i>A=_q9#w8kAVTSv%9S{eMD)C=~-bwZLX519!@0uoRhWi?YbD3Y7>?(N^&Tk43cG zd8b+^0}2W-n+l$JlvLw$P5siHRQ@T}iB^pJ)UxJW@wS4-GvTvlI!6o@JX8WVlmUi( zlS~3PWAI0de*mLgijXfAvI=9)L$5CtJOeFdLgM?kDE4|QRLNupdVvzaY+B;8C{Ps@ zvV@x?xq7^iXf_Vj$Ixw*^DN9GgyKiBYH`F-WTPC^wA5~D#N=f}<&G?Yo2e0-ZdD4Dlgm~O!`%^xF$quN z-<*z{xemL156JV;@`MMYV&1o$%O52O(NZRMyU^`7yXkr*=%{>&!B-5(V{oEGmkio( zfVjgf!cZz?BaKUSf6jFFdnu}qvPXVckF-<;lY=uS6(#DD3JL1^bX#?&ji%2yTuIhO zm>_n-XGSAh6%f4_wJS0e6)TE=Bo_QAc++B){uNtDcFW!@U{*Lc{5Lf&{UcN}Migsz zLj=tZdg>(s>|~VKF@6J}Ill0y?tE(g%gU}ER7kOn2-J5gt2=R|V$laDCp*jWRzL~( zu(vC~&)|`Sg!vrK0BcLGa0c!B(=l+s8vu@II%gYg&n#Kqv`D3(QTdk1iKA$kqL#LC zoYK9ARt1+H1*HTGr5;(5`BaYYJlw*h({E^Q-K2e*-J8gID4~1yVSrv2XEZ_S%<*N4R9nB$ZHA_>TJS zBk7pC)l=Fcq?txSe>Am=~ zO}XgmB?Y}i#)*$xPRko(=W*q#eR!I4wfEphdVdm&vI_lkEb2*8iH})J3uYduLBFAU z_dFC}{WYEnBU*2^I_~dxz2qjDW+Sx6`~picVWT){h&azt^I5LQDzDt}RR~qsUv+oo zF5dt>4rqrgMRuJrz&M7Q>EDitm?XH)mcr$Jgq;&N)yPVtt93S!OG~zJHG>6_r&Dl# zLPAlrNy1pwa!q_>7Pdv8%WEDD=a1A@$l~!%6Tj6(;m`+7TC{tC53E_91s3`_4u1~{ zgX%-37r?h8HHtG~mK_;CG6h@X{}tNxP`h+|;%qnvNrm=n|0~!KE#fxDePMQBo_^4v z0MkoNpa0aWAL1WwYOq0TE?Cf*T2{nBkNC%Dx0g^UoVu~Ki;Ph%urEMu3>V&DOjn%ABm4x$i2ohCK{R+W zNx>2~K``H7Q5)@8NfaK-jW;lh-TApbn7O?;4?L(qWBa2LHS=)XB&r>`T-D+Qv7F&B zAraXm+?Bj;Wtr{YqkiQh>7-^6_AO08$|LG;8Ovnam&o{xYu-4j8cWK09v3R995OLwG$7rC$pcq91UU=!5gWwuCsY zRKWW3IL1@oQxOGt)J^ttx$R@li|9JYg;uqaKoqY7puh!w!SQ;7B#g z?Q;gDZ)r&@E=q&U{n*A+hqKj%=^g1Arm4jv$I?lSFk(Q8l# z5}TtH(cfj#p%>_L?!bkAA7U!%??Cy`CpP>%IWJKg$+o;A5|2%oY7{AL)U&kJ#kc`T(qr=X{mPWlV z4dzgaintn)+dK%)`Z*;GR2D9HrMnD=d^#v}P}k>1TiF{5g>XOef*nqx#^xuI99AfF zV)=CJt>;L@klgo`ecn*`Jn=9D3Q1LaGd{-L^w;vzxv*P zoA9Hf8u5BQAnj_J3PtJE$ph!_;D0j`mG~I7-?@4DhGK-zOP{5|1tK*-xbc`WPaG36ZH-8YUNAo#NN{#o%2&C7a$VSV!*7~=Q-6o6$2n~V zh6Twd?mmjb8r6GU-a3auL3F1sXISFw9`Ab9HQxE7-^>Ec-9zT-$h1hFE-$<$f6_3Rgd| zKOCQYB)vN5ape8eLrS=`hAx5cJT$;E^Bm_G#hc7L!gu)n4G`|;SX9e%iiy+C7QanaP(TAD~ zCh(;>af?YgP6h;MQz$323^cEBy04OC-~Fg~rJf201(Yj6b0=+Vwx7>AEOMy-Q9vx~ zc(=g1Ev0|W>#6lrKy9yRcm7NB{rL1|1$r4nUw zYreE|w#t_N%6I*@RyxG;rWTWo%`(1;C#o(^G4i+RTKr^1ouF z1RyQeb2x=BJyFSm!p^w zP6Rr?mKwbOhwej^5h=Sa^Eq+9tH^yMhZ^?WWd-3{Gy%_Q^ScOs0V(dTzI7(OKVyYb zN=Zjuj!8s|I3rILY9my_q)j*df2uR7Nyk%r$9%;JwupFzhMR97_pec2vLpHlsBoQV?|mDa5fpm^2oYi=t~&BC z`{Ane_OkflKw=Kk5(Q}N2mjI`_UL~WHi^~S_<%!>H0Ib&D(3(k89U?}ltK7~^}r8Yiu_l{t^sT0TM>B{SY!w9;ZC z>7TGFTzKQ98R2+Hh;J*SIib4 zp!wa9HAEBq71@ArfMrtVJI(ilz|P)twc~l_8R}J(HEXn0Qpbbs0@Rmi8HesZNtA@- z{9xKhD)_Jg#y|4ekv=9=O?Ul85@r^JtWGpVD!{b52|O3gePYp>)Fa)!*yeAJGejSB zv8IN+gyh|{6cmVYI?|Ow!DZheH z0R}kgu0M2C7V-MwM=*W;y-duL1s`8!f}D}g#i0={o#rQyL%H*zx3dvsr=7gulTu`9 zz`1RRtVA>ic#*~wF%rIinL#9WO+07V)8Uc-TdZH^)*MavNK(pHmoira6ANLe&}q^X zlRLOM2c4S@r&(_X3Hs;DQbP7E!->X4SgisH6-Ydp93K?m98TGXoBE?rVERb2)|TZu zP#=J%P4Mj_JJS~WDqFE*O7Pf4j^sGtMNqe617I#B=t zV;vZ#w2-?_HnZ&NXpT;f zsPpiI^`7APp1>Vn6MPG=ox)qt*oQe}FQ=}=CoW&2Yvb#JQmnAid)|I*X+q%oA6xZB zBMINcEpHi_0bk7UE_KgaVXU1>(*mmVa1x1WTLTKTlnXmwjF_4=nQ28pC);^hq|mu# zPW^i|`Vp6E@sMtJHFd4TRDxs=!DBqMAA~HsXw8^i?BshSGdPfY^qu!pKpso~8(>=B z7kwA!$ae4+o+CWo3PqHYodLy7#NI!vj}R+*%dxhrmP<657~}Mjjt~)$NH|hA z9_PcfB@r}VAQQ6XMCFj_xfEn93VSb%rjN$yLpZ?^z0FK(^p(DAOO8MTQQ>LukN6D=;HV2A2#B-7U%!RE1Rf8`NP!x?z|JO2tbBD6;n1VUnAAv~0Yj^T9Xn*pP2J||`N1tNI zl~j1b{3Fo?sYVO}i476J%FTXB*#aC@P{ob{u-kGr!l;{?( zsRucjCNTbo5=+S@`_=k9rGX~D^PlL>K+)VAV8@(`^I`FJH%l=hTennnnz%@2=;ihP z``>yk&x)yBhPyX_HA0sIK8g|EYw574?&p>jwZ3Y}xWt+*{5OEx20~4FNr|)7%>gI* zkj?rC^Jjtm;>8~U8(gu3Jck|DB?El(cv4K>ST%7;OqW`0q{3rMWAEs*b{E8{Yyz>Su za9NX5$@3Vlm-FHEu_5T8*G)qtjdAg@&A+g1Fd@;Z47)PtPcoa(ebdCS1J<7iZ66|I zODF&F(X?fsEtW#u+}hFM#6y3s<^B41JqD)9x;*b8Jge#S9=}9?Hj&t<-B~0kZ^U#) z-`*Tl4el3&23*eC-1aFYo(vX=CZfAZqP#Y(3DaawJbIv=j05X6EEBF-r@T-vW^YY{ z(YGCIuKNSykF+Eop)wTK7+hX|)~+To>M`2Ke9z(nzNvx+fAhM1gsYEeTX;qEF=%t} zr(JlDj7GWoxPJt0<@7JUGYW03PMy91ax~%?2`Jw8&#&aop***;&-z|X0(av6%M;B( zoONlZJDS7cAAdD%T;DGr)il?3`{Wb?Ldj_M@}|2qKHxx!!rlPplyR&SN_Ar!a7enq zP%`AEiORMBC`ag2{iO)#@`82Sv6PT)`I$c*W4C#yHWqY2d$9MaFKTFtm2-byEwIy* zRz29)P3Eql`E>dG=ul*0kT$JLR|69`euY>?x?TUTEI9Q~z$}=3i zYCo;9sBmmrYL0zf#+~F(VhF|)JNLm#i@6uNpM<5f6lERH-h14v?-`W(GE85YG*-si zaamlGbaLix9qMtMCi{p;4WJ(Cb5L=S}M5iN=mEsGi1NL~eQBjYvPk6}?dbmJM zhj-;^dt)PrJiT0MN=v(O&fzxPW73Bz7lMarMqD;Nan<$16Dq3x-Ob_{qphz(3vKFm z*^t}LQ_hk2B-W59x}$m8j_uIXLk1B#g1DG@Ig^fjFpq$D@wNRr$f)yivyCX4wGNrp zDDe`}1de?b?U(fpTO=HTKFZ}(b4cQ`s>Hr z9?|;_iG#@auTksKaFSS#QG#*(Dfv0)(P1L(@#n?INmTbTMmckScG?l?QHouk-mHK_l7w|cg=!NwW3T4gO%3c9IVoaAyAq~%Wl^)i z#bmZrGP+_%&GHaWi|^al4UhpUQO{1FNpygbhju9|k>;2SdvB0|loI)phbT;N{YYh> zgUkvjk?{fFNUaceaMYd#=`es}u;kAn*GeCj7yW?-^gaW+c@n_|`7MiJVc$%Xq^?eBCM2feegux+R z!5_rekbtN`fL_dVG6}%G+5C6sveDF=4~A?O8H#7>YbpY%y(O-2MgO?g40zuwTtBL) zmBcDp>&qU1cw6;rK;N3&Kc5fsw#O+pHje|4LPZg7_3z{07vBIp#wys~1JnG)zJ?-z zNuvt8^0}^oWytFAD3Y2#>&pyr_N}zWjbuYIbU{H6-~PIF7jd{v9IW9(Em@0xDU8Wx zNXod{YZIk2Q%Wm&XLhY!{47kF`gzTdQBQt!%frU@u<=)qa?=g3w z;|!1Via&&BgSw_WaA9ZON4?Yx{vjiE>#njeq2xkHud`!8W+#)lR%&zDF=P95PDJ#FY`o{B26wQPBy}F9MoM4W^D=`+x0s{MDmd56JmrgH z%o1C!^J{28*)M$+9hy|q9u8bWd?aW&V|>2VSQ>t}n8dFhGW%+M$xB4+W655Q4NKcM zKnM0usUOb!CcY9eqv{cH=I-!P*-Y2UjFmlz1-)Kh!%R@@EqS3#W+x)siL%;;r_dGf zl`hWj(R|YAuOCVS@3g4)xn0v~#R|YgHOX1b`swBf^YkN&wI$?1Px| z1>-JQ$2(Pjhsuw3W%R+Kj;)^}LuB?B975MDdWTr~8o~Uxv^$bnjWOmaVFfEF$9>9o z+!En_eV>p~;WW@BVm(H{STT~!iyE8Q6-7Bl5XZ@#!;7qe|2Ws!IYkq~>^a5wFJxD1 z0-IxdoCBYz;XmfEh%|fi&Qe|Hz`eelZu`Sis7OemwjWk0P*Znoo`r=cDLCIDRE|wt z2PjOQR30BF_VI~z=wvTU%(ovtKuZDY)pLO}WESAMKa7f0ycY~~Z;v-I`ek||(R(<0 zDb-lM)Q7)LkzFdGiH~PYFgkJ?`r&#+j?_AQE8_B1d0vcn=ADtegI|du#6P@M@T;q`M zwg;jZR-frb&x~)l=K%}mS+yg-xFqZ3h{z}KIDi-lKzJNwgflD4*q@s2Sk;^5jXfMh zZOhm!7J=S~sKq19zDn%W1Y~F11KO0PmJr*jJrmetsv(>b0klGj6A%|(Ygwf*A9ia6 z5$is&u~-nvd_Q9=JdHR0$oPCW?t1}=8LHWpUufLbxl_7BFiP&EHU<8(PlE5#;tQ+| zWW)vvn^AD`lJ=^7O+&zu5_BC<#0`;ZS}PWr9su6QITDuZ6Ym}}29!lnZW&AIFksM{Vjmn9|o7tYlP7hpr3`NQ;%XEgfACNr}La#Fc=CU^9{eg?8;cpY7+5w zyaDb^JwKvXV{>g#dgki!5UiNdg*y#=dF`|MT0(Sj8rzI!)0rT0hlwbkL{2{sx$OSc z&cSrnMh9=b5L-S!=!9v?xSJ6$20#5|+cgOIfE+e&b zOJ0P|F-{DqPHTlU7Kg+^|0Jh_AJxcGv8~hJJ(g9bk`T5Cu zs{Cqpj4k%u_yi0r{f^GOR#P#}#G#COqGiVHUy}4aN3GwfY4W$nIg=#p2e;1yiV{}Q zpN#Nl%9LlgNU`Ae&USj)@o4U-Fl61jqmHaxVY;=L#dVbxqJ!bRG6e$BC(_bDQPEs6 zulax~oc!|aoergQrxyAYG(4p^)Ua7#=kG5FbiBFL`Q8{pqF&i`9JgW=SosdDKYnPb z5}ZG#&n>qnI1Q4R2X#4?qIbTRK2r13*pbS5ni5DZWz3PNdEeWo{)X#UqzxdBF+{4d zjmCbFT*Ehvfn#YZY#jgMwDhypj@Nm$7ciwnp^! zvk<<#Cg=+^Up!=^aA|3Tuoh)zK+Rt#;L;(EHt>B8_lm3L{W`@}aRWQBt_*>c#~!vQX&0QKDJi*Pzw>J;=LaR2YGA6T*#uYazrJGz#nKBi)0xI-(yO;b`jJ;t1Z zoM1~puP!%frB%i97B&5!^C%PrUD@n^E41IZz$zk+W5>Y%;4~dKe*^_^9@Xydm~LUv zJcY*Hl&TvLTg`E!wxVVC1}^*ng4HIQ1^|`GHXCZa2mn*)&y_8EAd{pg$3VaxkdF*l z39ok)%RI}2+0sc@q>!C%&59ayhKJ1MP>K^KO>(D(s#_NDm-#?r&MOeVuceaZqskN} zdEpmlC?dRM^(JT;KKGpgL5}%IgnQgKiqgnC>7+$h7tY{VW?JV8#|` zrXG!9&l#31ixXBGd7O9bf+$RBg!?6X9w$v@=i?g5y_lX3bmK)@n^#HEcG;4Q$0vop zC&P{a>3(ow(kA(difDmi^@^={1E!I`e?ODlTC5SeG?~g6q!LaftY-!1%Is~Vpu~tS zdP|8aHD#vs6jdo)20Z@Q+Hn6}iSt-8)csOw%!B~#!Y7u&pvIjku88RmPLN;kN{Uon zxD|Mx=7h3tm4#KK$kl5@t$F_?DV0(%3>ANrQ&99~JZZ?_$%rozqk5K}$UmPL&$C~v z_DkSCjum^&Sz+9Ct-C;SBS=x(2=2?-XIAWS&OQrx0XKyPXN^9Kg|aP&7pWdYUN#of z%)(=dHj!7V85XgBZvczg3h_)o@k+~cuc1_eM^Y+>XGvle8qS1aaqqg0s9e|N_-z{6 zF1pPoLHH6l;(0V)Os1F{E@KG?=OpOa%B1I+Rh2RA=RWzy?Hxg8LC_h}*A$CiVPsp~ zd>JY<5i%2^{Vk1_g7I-%Euk&7b|rR1Tsz3H3yL`!oS0S~8-1d-u*iLnx<7Ij>{2Is z61vih$);Xa*!466ot{D^l7r6*lW()Au8GIg0_)eBdO9Q;&|xw6ka9mfk8W|$`4uaQ zCevF_{XyE%IJ7k!FKVuS_!cE?Tphq&>O&NrNkpx%ii^4dfJGcq&LCZK(JF$Vlz-!MRnw4+5e9VhM z?)eZ|ATwmxoZSl#mkzItdl-UVx;qX|2DusyqE$+sv818Jm(?4faO;t4gD=sTNqXWn zW)hF#hS%d&e2;aPA~P=jj$dP3#=A7{1Gs-lNoHv9*fz0v{K#!fDCZn(PTHF1)KRF; zrLGEA`S`kbMYo-5#RijJ=3=o%AlS3kd3*kKpDGaM`)K!4D|O(0f{yphIJ*(FO=EH2 z6&8Mw9S15Y%?Vmo(#v^))><)cDP){0+0AGCj~e^QCikKw|D0+f_GJ zjOQic5UCJOL5IZ^2I!!cy`qVxS_FHVwlM#Fxf1+=P0JX71OC-U4 z27G;jE^c-_XMpc0vYNox+$(mo9`AhO8Z7kT{_@nMW1}eBMJOc;v5UAawcRi1cR`Cz zk|8yxW}7D%CnVO+40bS=a7|5x8HqCPu^avfyRL2<6rU7I%R)9t5WPb;z(AuKPOw`- z5U!&r2G^B-Ly4H~6x~{5sH4RB{Yy=%&1K#(Ueuz^V>~7a;0gN>4ew>39xBDgH9QXl z&7)>U<9DfFO-A`of4Z_Q6aM2w03#psg|*zE*IlFx!NzEaiDUwSm;4YWY9s_o)2-y4 zD}(XfLXwDe9N(q0PbkzU=^5jZdRK;;-_W?H;TQ6x#9A&lfcO6k{**D%rX|Ee5 zxGVqX(N1#yynZ_ALrgBB;_5Nt%I#E+IaZb2o#{OhLgXz{1xeq~Za0Z@bafXzI&L<# zaXICBc();6c2)@{OYO-&v&PyaA@I4P-+K|=_|IL179t?9DLQHFeH`%823;S6jK$ir zQ&Iam%bC<XFosyeLjpJAH696Szy31tVj5~$Rp^ne zBh}sn5(>uqRrU}R5h39X#-GQ*o(#*VKmJ*pGVPA!5RGq zun}pfK1|1C9UUj2@@haGOP7+(0B;hdh{r(6*oezZ01v5bhG7HObeE{Vom!&XS$A|` z*Nw%;JNPx`P~9Z+_oFwyj%#TK&fHRxG6gSn8ZxrANo<(hQEwG(>#(yN)dfWg&54y4~##~`YW8A49{0fC6Te4S$;ibN{c8XB4 zJ@w7^XKk_YljE~x*N3<;=>^}Xq|zyQeZf{C{~%ISYk%+bRA)xq5dg)Xvi1ctO%M9@ zl6Zsuh3~s9&%n6gURhxiV{tURbI*II9uiTgrP?yPG#b={`AbD3 zXki!p(l7WU{S~e1Vy5G>)x{+D8D&QaLST%^%{rESjL{aGR9YONF&aPwof-A%n4|X% zqaqD`VDRs)hy)q}GrC#NZdX!p4TPZ4SH@9?ub)SUN)nmv3_)?N%oid3{|gNY^7X?- zRrAa-h3+5u9^miCm|;b7b?OQ*KV%gF!0Sucog-AatAY5HbZ z?3EH{pEA{sPCP*5pG>sh+AX3Ve8tc`OB)1EAyS@TMjMz9v{6@Xf(>6Vx!~etw*5iP z#H#wKgK}W^A3JP1?pP}CKG+Z{_#hBmBW)Nm%BPCu)IhAI&fu&HL9B^fa(kZJfEtV! za!!Dr#IC+^%yy>SY`$1P^+Y6}ejtM(XMA zB?C{Gt!w0laTCA`iB!2k|f`GQ+6vl)2X%fxOKT|5%3mU@9V z#KGoRLVTsp-eL1WX`6T=upa)eMnx0!_-T8IU7M_%b^iBA(v#Yun!39c;sK@r#hHrBHR1`* zrD|Oi58@u(##Xvq4L48mqFx$)kEJfP6mM#p7)Una# zk(rF`Oc9X3==;Q_uKV1it8Z5;#0l8|3W3C@GfQ7H4V=Uqhjf*xgupuP9Syh$JjecL ziJvTNF;PVun2sX|-042wcvOTM)TFJv$`@QlKO%|0oRP$^lzx?3C5SbMKR{Z{NSSL= zhfExj8jInnm7?kY0C43myNzY1xs6B9{E04`+!F#AR*z5?h1S+k);^-m!YaH}&~pyP z(j{8MI?uE~fu16`*>e>d_>T#qI3blT;;uHEfmiV{F6as5%N#0f`YN{&wYUsPp=Uk+*M&PKucm%NG)<<{9az{WmF1)tdKGv&9F|23Zgd zOBikmO*7fJGRB}105Xu&Y9N}0<(x_q$!RzQP~i}B5)}oPa20|GTqS}k+)Yg)UOy1K zm{7qgpiLQz4)4@b>2)}wVHxgc(Q8EVCSMZ~eZ_5cGJ#(Nd}bPqeqacs-3xgOy1=d@T z6z;dTTaKd=D(LGHnFr-DxDb?gkCwiq~7}cit z0MIs`&iI7kA4p&(! z$1DKqG4!C8GZjR#rx6-kP#KI~-_*xdb4qiF6bKLv0^}{KSFYm z@rQy zW9BX?izx%W!Gow~nTqSwpjo%vK*MebGz)|!f;U)?!zKcv!c{wvslx;#d8EteYE{4$ za^^YSV2|L`04q=+TtU=S6evPsC`=SiGFrr0mkCBl3e2D!z+fmb0@;ZEA)o3j1)?F6 zADl6e7C5eDx*#Ei_WuA;X6cp@U==zcVD6HjBU^O_7B3R#L>ZfALgr-b8H*JRLUAyc zva>pu)}v(Ms74MY**JqOHx`PHRV;|Olq7>i-EIjeK?S|Qx%S!W9(jRACZ`Ds3`8ci aER{UjVu5Sw1@ab6J5Nxn2w>&niT~NzPOfDD diff --git a/apps/www/public/images/twitter-profiles/Y1swF6ef_400x400.jpg b/apps/www/public/images/twitter-profiles/Y1swF6ef_400x400.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a9cdcd3f266c94f747b25dc1be9bd132a41718f GIT binary patch literal 22380 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 ZCZHmDA(vLAE61K?xE~k9LPzQ+|JiI^W8?q; literal 0 HcmV?d00001 diff --git a/apps/www/public/images/twitter-profiles/_iAaSUQf_400x400.jpg b/apps/www/public/images/twitter-profiles/_iAaSUQf_400x400.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88c7f46728788a768783d7ee19b143021404b09a GIT binary patch literal 24000 zcmbrkbyS=|(=Rx|-8Hzo6I_D34LZ2H1b0XX?hb<{xD(tVSa8?DJ-8*f?c{y$z2Dih zd-k8Lp6>chbyrJGb@$Ux&)f3bHb6(l$Ico6P*DLe0{{Rd0NlIx0C*^R2aRIyNd6C& zea8rZ{f`_50D!^$U-)0C|I$D7f8yH?%r^xoDHC;|nyiAd4B#C!2PV z_HYBrNmA;8^eGXyp;~0nb}0dHW)|+BB-GWF|96%DPky`jpEdwDAW z+%2G;M1qzQu=wQW0mUDoSk>F((?6UD#dsFBW|mM~1jVdw&<;ZJ^gp@zfAQWwZ1Z1y z{|~!q0i~d0LxW;UoBv|gf7txL_}^G4Eo|MKpk*APnAXYB6WWLW&_8RTSvhNKLOJPw zqB{TxkON3UZD9`Z1lR!_0UiK0D0hb1?EhVl`#*ZB02io^1vLEx@CCR*HEaO3P+eB2 z)Dz$iu!3@HsN4z~9HAwk^w0MHH~{cJHg&h=`Bxuw$Y_cH0Q~OT+aIV;AZ7yqFA;BV zPX%vpFNFXA>@ooGGxdM;T~eXuJcr5?|3^lf3jkn#1pu16|3_wC3;?u1ZA`TB$;{2{ zKkdLmQy6Qg_g$9)0O%k902gX&6odc2`Tw;ywC;cP0}7V_04;w2KzRZH0A~RJ^w9P& z4c}G)p#V5|cw_|Re}saBjD&)UiUN&jD5%hY1|@X#e<>O!1|}3^LJ1uM8ygz~8w(2? z7aFk9-@(DbAt4|kB0)b0&;aF7jEwk?kWrAK-;w^qpgI5AK>1(opg;)&3j+hg`|T$H z8|hu*yF}P`)BqUlcd*#+-Ua}5&|d%!_TQZO-vkc}hk*DF1_^-t4gm9i%K^Z`y@P=V zAYcLB!NUA21&4qHi+}(NfPo6&u;Fp2aH+W@5V$2Z%v=%iXn^Kll6c&{*GYvW_s(46 z(`s6{7t~+%&2AG&e@)rBMk3_ZvMlVM`-2UDfq4fD3->Smw;Diu1Otl=heJip#SM=u zA*lhxGdFYll1#(n8dC7K0ziX>b_^R98z2rig_tQL*SpEi`I`68O~4Kh)7SgadGPMm zUk;zPVNGlM(sLUzR%u=L*N77Am3c&638WE}{h9A?RZ0otRK5|oZjT_>6!*}ssaNMD zWmdA@dg!6Y)q0Yr6PPco1vmxw3P#1#+f0W&Nmcs#1l2oNa$gRzCXA9T*beTMuX#R> zu9mSxiQdB!C-Y8`VfAl)AM?`k>wP2VH(32Hz}BFGeo+ORLt=q}$zcZCjSVqDgl`2b-Ni!BiicMPUX@~6gbnxnV;k=9lr zwoS8x7oROo3lx%Vg|v21C26f@vEUnUp>|yQSDjhbTRG6NlBrC`@|uS7?u^$GMTJ#+ zj@Tc2H$)v4KVl!D9{6-!}j)Pc9QOmjb!j11@b-oJS+d>4%h>bd!*qMfN6n zNd~g%KheYW3%$9T?{HzQUDasPPe4>EPHCvj;<@GX=tl9LsO$?w{41XGzU3 z=k-f;4_%YVXP}yek%TeZ#0YgPwf$z?GMcAjjGu0i<8fS>|^fXXzA?)<=#>Bhg z>tlJxF~t9mF#lsxMLowUhm&T)eAEut4-7LW2!{*CRWQEN-%*0n6z|jhSj&U~mDZqg zL!}Xun^Y@-RMV>qPdRb%1)LM+DXc?hhRFSr8-+dc#t-jEvl%0waL6^**z2s0)9*UN zD!Q30J_Yh+fz}G#k;!l7W*OqeNM;R=7T*BeKbVDZ`ACD8sHzJ#U2o@Y>epYdsTtn@ z+Eg0Tow9nvM@N$-BI`ac&2#(?pWMQYYk=4wl9b=BjTN@lNHtCL{>D?aVv4hUq6B}x z5(XdNTS5XKL_fa)p5%i^OZE?0im))7qS2C3FVNlq=2DMcwSOzx3XTFuPw6ZRm8&~l zW-tUp`*=Fv>Cgr2D!K^DW%wNsOZV7tmnw62G_+t=GHR;T;e3^(z zsjCfirM(jFptZuWJjl@up@5IDN{($4Sx9i`C1K$67^pxKo*%cc*(g@J^gkdhCA6T>3lXJ98XOB**CWdQ=1 zgoWmFL*L|5&<##S_*%7UGd=y?l+EsSgY@JHJ>MMbogPlunx7{9IEI_$$ zhh|Z@DR<~bd;J)xiqS2AcvVJ@XoL!oZXLuQ@TI&fR6fb%2)!>e?%Mf$J@?crh}g&B zE|pwFy4bBOi$bd?!b4W&s6x4G;Sac1XwP55hr#v~bU3x=gd8 z8((<^r-nS3IM$ql_B+N2sZ{QAJ*xGl`NZQ$IF)upj^zzZpmC?`=IFUdn%XQ4nvP-s zkU2SCsH`wt;11E+;A!??;}hix$`6Nc@kqAYuZw)J@3tHrr-J=^j@$X1u2{D_>@(E+ zQ3lEO>Q-miEMe2n0y0FX(QDT9p~GScHsCQG&C5+#Kn4pUug%5SPX-0 zlayzic2`FKrooWZt>;%mn$1uAyR7v7mQF8H%-Say632%tAg(CDQA?=@-75FHL@7A5 zllzz{sDFTy>KYlYGP=l&erRc6GA7nwW3)<_jpvRv@y)g-Zfp=Y@UN10ZEy^?F+RN7 zag|f(Lf-LF5mZs*#s52PldO`Br|J9#aI=6=c*JZQ-l>DeWJhVturDb#jI3_V>$^WC zQW0&(XMJMrkPG}itX62iXLRDJtvwek?XgZ}o}BEWk)bJY>-#OSM=Si}#A830i$sRC z=wUaF+E6Qlw3nIF9?WEq$eM(EJ?EXxc!fUm1{iM4_*pIN!h(xVUhxGOH=?&}dZS)V zI&-N6f0{XI5*^f+sfZCldueNhE+asu^A*Rn!APIUD)W5`Lu!A;?dV}KD@u9|z{qXo zeF;hFYt6-k9iFeoa{X%Z^~&?_uaib&G|69ot!XK?E7j-eLcj~YT8~#oh2g%Yt|5#b z2Qq&Q@MzakOOTdZi|wfq$dxoVe#1vEmKn9xeq^`%MY-saOWXqY2Ji_}pwJ7qv5^jO zC{oQ^OExFa2O~GqHPuf-QmA^e49qQ8P%E((5hazMAsi!c0>y~O?fOhnOR$=tpE@Hj zq$6yV^_vb`t;`mJ)b$Py)DZBTd0eg*1B}La&O!ToDH~%8FOB5)#=WdK-jwtUcB?}EEFh@9TiuH$_!~d}E3#(ZM5@V6$J6?=w$a*|Od1EVX~j}V z+FgXLzWa%IsI8Ns4WI3Dv*qpS`O8|2-*H*lsuKgKwt<{o^%)}dDf-C4Di5S~`X^IX zG=C=H;ZJc@%eAPA9!Q#{zrx*%-$XH@jdvCT-w9X3Xig_MYKr6L1wSU%rW zl_d&=cF0KdqC(^WWGK^O^$p-jXzB-PnSVlx&Wiq27~7oUc$=*7==A(6z0s`|TQ%Op zakL=05KDALrRs|cvA^ig7X|}98A}U+J@lwd42@(Wiv)3-;5vx<<7lmRMV8d@FpUza z#q!EcnoW#_f#0gEV2I+E_A)8{@*j+k4{;umLu2HC*ng{3%SKS(0#iutAftnTX|> z5WhWI9JZtTA(yS7s%!*dK7vn!wWIV-%F~lVj?wN%tC=5Iz9^#{!{uePrZ127_^ej7?y(H+J+H!q=+=8HFg21&Eexd!oINJ8#xuZ+F@zCTx>|%Q!Fn0~o11D2 z*1JN>=D_zF@FO6iPX^4P({fu?LfF$nVjD4qBBN$~lDjunE6@1*f>vmi3Ig7m% zmQl2Sjb26U!KXKWfItX}@y2DJQ^RU4;~M;gVLSR>NBq~oNUHKNP&o^=pvXp zcQB4C?svd6*wG5oqPe38u|3n;b~6?@AV#2BpiXc!_3uh}aIa`O(tqE!JJLXzZ6iyu zzQ4iCkCx}k6V4p6!Wk8AbCS@Wuw#CWFZ#)!8*SC}?48v~;KEGCb37!s(px?#=iB;< z&2!j=cvia6AzPFL@mV;b%yxE9lm#rmK>f(@~wpcQvFl3837-sz&jUz zaVz?ulHiBw(^gh{j@!XP#z;ws0V77PG%W5B0YyZ@P4Wc8fXUPYA-%RG{TAqR?_X76k>(LUo=0)vAMLz9&G zQ5PL<)Mpu1ilWzfG4}7)+O)YyUy)*)Z|Lb7C>owLIU*_wJ@2(L1?2dva{3v7g4aWo zm~AVQCNPK{9E`DSA^{)>ZyJ$}f*Ez>(27?O#4#f;_?BJrd*|eHL~|^u|M#ybLZ7`;4@0kNJH)hDra2eY49^S6JG+P#v=X{fMT=ov_+h+UrFi0>%QZ_iGta}Vm9lQ|BiMsRdo zmNOv%9rdQ{Rm>t?mhL+gwSsrFSHnJ6e;{>~JfEVH=jgdec+ke56_=RVInX9=I_t(J zYVLpJ_-eO2+3uEB2uTGSup6$f5hM*0%lvWn^zp8i*M5o3cAA=hXfT=aeH0c+y_TWH z4ffdq!Cr*SW=ehT!TiI%dE|^w=0tm?WyzL*gjx5J-|i6B9EKN)=s45SGJ{iFXX5T+ zuKb=KZxOhFe$ypTVxzqi*V+KuONK3;H?@BT%0&8H%BdhNKhW)#@*ZUmN}3P2Opt|? ziho$m@KSdd=y8i#h)&J!N_-z~1A1((>xEEhvmSE+1yc_SNH%3~DoCT=TTKGKGJPLE zc2HA+F~ftX0xC={th)ny`MqirOW1Ybj;bBo@(m*Eo97g1#Ume71X zYT~6@ZzA3jIqtxW;>f3LUu%Pv6T$vK*xEr-$os=o>#7&kh5R|pxu;Vx|0n0v;kF>a zZ6xirqQAE!S)pda=a%NT)sp=izyyb;_<^4LbIRkyM<)!bsaxNA>g(A16G^fuP6d!B=(=vFtDVPp>m}aTGVU zrz9#{90ibNv$9yFN=A&~o-O^GHq(K;jh~}J2?qAjQe5SXV@^(-Yc^qM&pSx;dRICBF(PAZxF6YJtCk}OW20bb9{Sx4 zh7iYPg6Msh*dEZZZy1ET8J(mfeLBji+rDqJhqu>_s39m^Nr4=;9)1j~$meNo#@*lw zdVEoeK7wVUd~R$e14-KVv)r@5RCu{+-?%yq+Gv)i<@T1QBjjYQFsk^M<7+Ky=zks0 zttcf-J~D+-y)zK0$-&|&u^ibSMa83Z@mK>1$_r7W$5OZw0AcB7N4}sD?<6y3w$Xu3 zn+@G87>Wt^#%L5LMp6dTJ#}K7-{Ud54yBREr+QnVC4NlH6-IV$6sXF&(Q;;ZM+@fu zXkG7MWQaFo!*SwuF_#wsYwX&CkSf-LbxQENEPcc3PImt0 zvyJode2pbXw4;fx*d8Oo0-RyG%|9MX3t7*Br3UfUzqe|cy>W^^lYJ6ft2dT#+F7bP z2FBd>j`92D>+HCA8xCr)29 z`w)GyIWrdW+n$qZHAqaLo)1_3fgaeE!1D}28SWltb2FhH3_AmaWq0h2oD)lG@FR|I zW^g2n%a~2)HtqMy55P5op_0;bV~<0+@q~H+aeb$31$nW$E+h1=f(Wrkn>#X9JfaSC z`P30*MYb4|q9vDJ&Z^Pd3m7n}>)umcPNb<{@d_+h*Eidwe-oCZbCT@&OmY6{@D>8% zJEc=Zo;5p%E0Kpgb$Xl{^wY}Sz_Z_&X0e7(oIXa3BjNF&=Yq@LpQV4KeCfI|?X?MX zg09aj98yJ1P9ohk93J6_@;p)a+5FF@pLGPLB1W*qrhE#goD0%;8m;xuyLTTN?Cu$% zXT|;U(A?qXuU;+PimWYV@2gwK=vd-CJlEV3f5~uq?%s9KrFcrJ6?irYCD!5c1fkp? zk+PXl5#AEGv@&evm@QzRHZ{8p@}S(k#0sy5-rncL#e29FB)U*C7x*|T)eD(uVD*8j z+PrM_rC9j!SePLzAJe7;XsAJgG>v*+VN?|<5LKcRI->PQ=hKOdAari#%DY3+30ezW z?h;nG=cB{6~?xJ5lLbvO3DXl{c{BH-MF2nZG*@&`$<|Lvx#e-LRxgupnD5?GzN~Ws-MiP1`&jhYDKQXu%~T~toh@2n zgpT#ytz*9d4qLjeyCZYicp7|#gt4RCAE>b##98iZ^ev<#opv5uVg+u~!WC#P(j(Y=D#QmNYcW-#58DU1|f4IvUN z-<&!bXqwf>2W#5zS^VVnx)tk=@Ia&<&fPA_lM7PzYxYM@>$ANynhm$6w?EM80^qS( z#1gQGVFU%!x;x3b?LXI6+?Ww_cZ1tbY<4!7HRNUy&_16(@kh1U6DVnx_pao!pDUI4 z{j`HK`J4WVLS>{p2;_p~J2=&@y#ZcyO7one)+6;yDpxGdgW-80)Mwa(t76sEYlpbtcYOA4R96*9tD1^Lju~~+E<7o}dnZKe zpXkT)6R&bAMCuPKC;nzFn3R4clrf!9w;M>I!JCdO7iIY$lQ zPRDn$e#s@e<5pFcUX^<_;=Q@M)bN% zw>Y(Enw24&9UDo4>t(=iqy2Rni))hXK&upUha0bTly_(mm;9YBgI*JMfB##y5M&{; zH?5L`ElsI_W>%k}sxXV|j?8sZSZkw8dGe{2eq3zN$ZBmt#b;{3v<|#mi@tR z2NiYxcpU2RmHeXYlToEl2=+f_2UwVe)pvPbK^EPK`#C>_J+r6;0yr87iq1H! z;?UJ`Gw^l>1$;4T(q>Z?y57m2t8M(eQP<2UsnKwJRzc|{dl|V`HC%-hsA*vr5BV|}FDH!r&h^yIM*3HI zg25fTZs{o6aUhtL`AVceJ3LD7IIQ)!@`0HZ1b)_Zukm8%sJQT`uZ^W%LH@OCvlI2x ze3QSvin1o4zm{S`$?k;c;YE0c#ykMevuQ1VHpQ;1KU$Dv)%XU!xZK)*y)(5wuizOn zQ%TSk1oHr1(SUCcP=zuKe`nS*-e6?FZO>q3-t8Fmi8y@mVZEEYt+}kK-O5fO>VYU#+W(ZFs?p!3lh_iDZXPAp|wjZlj&|# z7qB^~Zk$m-e7PP$4Cl3FvXvM--n82^L{DEGq{<27a5bJEXMjkkY!$LP@djvnnijP3 z@}~<-*2LekPYWtPnYvwPgJ_Bk^AcKb^JF#!}35MZ2J3rJBq$?w6T^q zzXX-=m78Npmmgj#20eJcf{>uk4`yAat=yn}Q4 zE%9aA``SdjU6sWC9%XfwG&uky;XTQDTU53bD^rW#OH}T}9AbJrc*6PGYSiHpQOsaU>(ujv0pUFMJl$XWHQNf7Clf1u)rVRqdrWdWl)eI>H!UL1uQs3X<3f% zT}*HxJ>vz8sw5do%Yu;C8iM#V^s^*3=Aw=5Qz|B}_TvRE@<6-<*XpOb_$az7`k)Bq z4u}Qx2++7egY93lIz~2zpt?6N=PP~8b&$_KnqS4S?m0&?cIfe{MRV4z^^Ld&U1p3Iuh*>}56wZDrX}R+M?vqDM3#Tvi zWj==IYI`RMm=efMVMjM_nB?~{)I=|*de=&%_t8O3=Ru;`s+7+YV< ztbu>(Qs%_)9A+EYOE@co94AP`;{xczB5RVh8%sh_QOZKu>JOaqXu$Svsbs=^>Ydd5tNJ64*xoSESU5=&^>y@%$0mBZ%9kNHFwI0J zcg?rKieIeb%DZ=2r*D9e8Wz{#-c}437P|>@@EMb%=Po%@ZQs7 z>9|e2xC^KJCdA%7*7O9=`?iuVI@v;JVk~Y@lJ`p|FsB|X*!rpY%0R=TA}_P0pnR+= z^>tcnvf&;02ZS1Fif6S9lEx*h%ndl>4mAkFnX8FZ^V((5sK`|-VNf&^NSq+j>Qey< zIq?PCy#dmz1L)fY-Ey2E-4u_^g);{vnkI4@AVGiAlx0PeVhWFLypMHkk`+0khMzZ= zXc)%k&#F4?ARNn!v&W*#LtH{On6({E5B4Q>D!Cms<)?=i@AEM#>9I#H(!G2ak3PmENlhM z3Cdpmk~*&`?EhInMDG<)qzrNy=}?M#VcH>YikTP2N(>8iBQVp?vi*A|@&+hpTT9eC zhEHi@mRU`%o%|M&HC2OK$i{MRAvDw)+Ol=b?3&~zi+r1K5GkX)KGJwgliZ8F+~Zy< zg&8f*%F1ZgLG2HwwQFek5W8rAU37g6J-R0*{oF34y=8I^w^B265F(vD=P{J*<2Sys z36fvH7xVBbGhm*|eKJN_z6?Yvt)UDwQetvNZNZ06<)JOiEC^z1zi{~MUU_M#p~dw_ z^}^F&)?AEPSH*B}LKqbIu95K%f!wOEhOwnsx^k3%=ixT%&?N{3Ek0YTj# ztAtXmsv!d^Nk64tJt4exv0MODJ`DnwUUxP=lzUYs6|y@^Y;v)kBYNQkQJnWO3*xur zi(b)^zs8@PDiGONh?9i7b4_+~Q^9vWC*BXVDAtc%U_1J5IB2JK^%WAhv^A__lhAPu-r9GGpAb;d!&;wS(B{aa|a8Y zyGmZ}*qHscmMyk!!C;XbnuQmW^>yd#To9bYh#dj0>Icp}Ka&~G2#aOt<**gER^9ZY z6^DNUTWAY7b_j+06p>u(=n!g0?40>-hfB_#(ZK4;vd!p=sPWBGa-;}X{DIChyaLd7 zCcD=o_Vl3d-ZEFzecy{pO|h}j;ucUr)qA-+xderS=EJ7M-2_XYwkLrlrDaDSBsPA}$09X@-wV6<_gJ?!LEv1AGZYO$m>Q=;DP9GlJ`1 zBr6K8NLw1T7GHLkUI9|^s$>5B!bLDGYAP>wVWjqHMATrBj_io8VvO)-g+5m@t73jb z(_MQ{W7Ucflb07kAo|Z9J8RIa?P5zVMd%JXFA)8XX~(hnvNl&>epMel)vdX|xM2j* z)<7Us)#mo3Er3pKm~BU|3@Z&0M^I5{KL5~F!lu4){sP%Aj&in8-XWf1^N{ECpG(2C zL0Np~`pyBLEZHUM=2nwgu2VafY zmcSFObENbVM-v1oNQJK@1#U?70r)Ii#pDdIAhYbZIxaLn;;~u#kERGn;Wn}Z^3)u9RAypIC$=|!mo z?GvduiR&vTsaG#~m>-<{-vBrG)*1=DSizI+L(IFiN&`~#8cyp$HAzcVzv%D2PvPVQ zn|B><@M*UW2+L8Eoja7R*qAoGI%L0)d3cth#C}}m(u{(mx!TwLIg*FPatpFz2g@(c zZ+D9ojxYW3h)LJ&SUG!|Q)%v24%1%f2}ai_nC@vwge?5vCp-U3?zc@kRqo?mcA9qP zkaB$SS0lR0gk=TVE0szCh84QG9YGTQ-~>!~ii%(HQb;@AkEOM&l26JLweB_EM}$STTdwBRzrQt? z(TOX;;)l=wA^)^7;^Y`o?NBaPlsVrJ!Cu(J8NEDlUH3^7S@wB}*u$nsQxJ*E#kbP2 zi&9eqDNfrEJHMlGVaB3b&CQj5oiGN-)))A7eYO}#J1Q;}+Z(NU0As%Hw!1zdYca?z zR@)AJD5z}nXha8YVvf>2K9&}+K9&{X$I%$@!_$;z9S!sTgdn8DXf)l|2hcd%(bEUn z@!QyRJ+L%0{m_dabZ|XrN&Ks#sN`UoH~U!Ove(n>EMwghvJS;wpO&g4g|S)IMp-Nzt&j{Z6(Nx}Cx zPa-v-ZqY%y;zK4<8dJB#v6?S`dkVBgNrhDMn^yB}wm|7O*?#&zb}1UiFiSl($PqKd zp21RY04CSHU7HSZ_&GBE$im*s-V`sJRYi-tB|&+910hDI8it%3dw0j3;vINwFYlng zUNFk~ysd}XO7w=dL$r}(Rf#!ZZsMx30LZirkcAm*96a%X+1ry61?a3`6xK$!Z?A@g z_apKU!B|N_q1w{i$7w{)o>w=t1-^)w8r@NpfSNR#@7wWONj)_00>a=qrdrTT`=Lu2L;wYdGPDOoaz&ZHhF*8D3{c`= zkb-o{sQbww%ids$<-oz7ly+l!&c{XXuC-t~Q;h|l1VpV|+UD7*(gi&qPt690&IF^@ ztr&+X#s3*1~00(TydzyAFLi>4SZ0L~V%>Ua_= zJ~v1=nYU5AokYD8{5|tqg_%*$t~9X9Y5qd@LV*ePeMtN_5Zf^}R8FPk?+;Mzefu%6F9*xsO89OJ*1tUt21|}!> zB~ zU}7E=8zw4sZEROH6r~pYC=XXEl5f|1si`_;TH(I$M}^gORgO~F6o2Bi7OgWH0-YPb zand(=)-TK;iIsm)631^yn;zM5eW14hZk@bmB~7$!Ey|B}{=lkFx6TY8S?;n7GuZ`5u)k8aYavh~mW-#3(q#$j41oT%(?sDyd$lh14c#6wnp% ziXxS~u2nW~AZfi&^)fnou(+tgHpe!6FuIW_Nmhd*@kJ-jh<@7WhI_9{C@;?RI&b=r zlKR?_OpX8g7VoN!Men zpT2n?Fqjzv>3CoWo?)YT61Ep|@KnZ>|1L_ld97wTR5;F#OY~ZwZx(<6blT1^MhqDx z&dlz4c&IcF&)hQe$4GzVVRtVax^C^M7C*shRs_&ntTzQ0_zUbgCQk6anYGye%E8rGL6499BcFj7s=(n>f{@zlsPi^ABz|cODIS{-!Kb>l0=P-Z39{(R|ajm^Aw8B;aWJ7--#hg!*{6>2h|=#1w+<^ zO1OfxVyf5bG-w&JeSbaUX5l2H*7mBkM>w=xGf@;ivp2-DihIi21!8aiURc3QqVchk z;*(o}UJ`|M;)A7h*wPyK?#>O12_9ISOOytRnY-}S5N^;dlQL|0ZnWeG6N)k!6KWhQ z%%;7PYJS>*v(wAO7!=4Xr4hzYPoFakka~!s-vG?OYbN$+3)e}Sj?OngR(qkyc7Ke0 z`F*>{JdZ!$y2Zu8W2Jf486i3PAcL{aULPcUXFB#vga7^U$o~3jw2Me=uluD7?<8|N zeS;(Mbyb^oiPb2b!2(tHa z)NTxq9VG(_!Iqzn2Usj78o5gUgk{u#<*PJUbT)oW4&Sz3t%epXpMwqJqgF8;7@mfpr6Y*R z*U=9Q>j1%4)<}Uy_-%}hiMT=yw9t)VHmaypRv3+i8}VD}S2<7N;xcoM%(Al)Jft-o zV2T==JOjQWkDXh2Vo3Ucd_b{R5M$nahZ9Xeu&I^nx(*T8=>AGa z)Zhn(9oc|^8sW8uo%y$F>KJ?1k&@2b=aeQtqjc7?XtjANt=)N<9ll}xJD)50O#{2y zj-BS3?aDrGc}`pq0v#J_O0>ot3+Md;#R~W(xkKCa?9<7(JC6R& z2z(mv6TP^nI|5f%L%k(r*)zD~fvL7lA;bm;AyC{eUe$LkYYqSN<$)8st~?(O;vbb? zoiP`|bgG{^T10(H<{N?U+~>11=v;zjKVOkEhSB3tKL^9lmQo$Jv2QWIb1V!#7!8gP zU3MBh8Yhxpm3YSX;apEy&raC-t~H~uP9SU_gVfb(d(=M>NRnk}d*|O)@+Ssx@M?q$ zaLWfUQJJTRo+{izRx^Zsi@luvr?%DzXbA|mR_LlsKGy{&9OS9dZJP75 ztBtO)y4#yxl@KH;#E|S><-|B6{^V(_2Qhz`Ll!l7H)t>ZY(r7Ok1cXexj7Xb|ENRw zwctMTD?jA(bIx32jr0wVJ;UVpe0yJT>-{=uWTVEY-pwB%l>_F89S=cHo_D)+e)P?( zr992AsuM}NN}>5&U0SX-{5dy|a@uuwIZK86S|cB@8CZ$A&02>*H?c3P2spWZMgCBZlr`pXlN6yfenQz^30yz#>K^2T5H1L;Xzc?A(E}?36~J9!V3f zTw!LFzyPVqhs5?2CpBA0P>Y6Qr{~vC6l`rPcYC=JMGJY93^o;d>7;$F3G!yeGdnJu z2RYlzXLOp*xKY<6cgzk1HS@OvlB&WvjaTX`@81AAj$5iv{132havgJa@k?g|I{E6& z1{rNMB@yIk12{cf`P$~x*jJ+*D_UplJmj=Lf1z>n9|<#F5p1vs))BOM9*o``o}3p* z-|*4J4Mca4QRq^8nIjOM1np1hCCEc6Xp#xmC%S5BOBG!~Auw?hh0S2Lh4yl36ZX>n zy@J1mWIg7#>kO?Y_biCmB~*OG{66kWVQEexVan#*4f~V**=f@`Jj8w)ao`3+5-tOo z4+UijcqoyUb$Eil{wIiwjnnn{o^ln4MSsziPdZlCXQ*D-Y>8rc_y@10(VJ^aSsViK z16dRzx^)AW&RiY4ng05uyN(W~b_msC6<_-g!MLADpc?SQ)#=zle*gZXt)QDt@W7WR zWYEQ|S0cBRpMbZo^Mkvbpi0|}uXb#Zu(aC7pXXAmNN;*GuKGHdSIhOZP5)U;U&zkj zr?}15VUExbla4M$_}+&r!-oh9Ba`qA{u%v7y|Qdj&#EeF+Q`=iow~xfOlOUQqXR2$Gtbi?OM*bb!;7^hrp*|!i;IsZEPV`3 zt0uinmeYP)(~W5`9F@=We)UY)wDyT{Q^T4EHc?V z$w;irviKlzFN2jO3IcmW3kps20T)$=%~YY#mkGSTiEb@Vq=UUw_CJ?+TLVvpOYZY! z-z&3s=X;1tXi{jXT~=up-d`bBNgd<(PoMt^uIy~uFTN8$n|cF)$h{wCYe(U!J!+ki zTnAp(2n`qfxQoi)-S|bBGjSvq-6rP0(%R<^1(NS9Tco6MsZjKwp5f>mo=5n=S1wp^ z)TwoP>0=2O)DP_3dbx722nXb{;q&0=3Y?2IF%<~bd+YgV%^Ze-<&Nd(qmPm4+)JrF;ZCLl}vwK_bDvod{B)c2P zU2YuEzmYDP289uOl#Y(1>DDOWl&K-844d0G_K5=NEe<04Hz_EjZp4nOtQQ+7%K1RP zT$j31%yH&u#Dux~uCyz^r*vc^W}R+~S!!08Re;AK1xZJ2r5o>MQJ0CCwr9FIyYjN7 z@Zg^LVW^fq9i)X#MQ9NTO#Jn>s594R=pH_whs2ALuN|eLA896UfO}{!{{SA7FK zi(9J71xf=?2}YZ-{k7v+*91!Ac_(Pn&1g zEUI1T-x_Q}vaPP$&^P%BPHh$~2_rArS-zosr3t{)^x9(! zVI!>TqGQqds2(RubpFyim;4{}Rvlfh!-^73wgTW*(Rjn%t`s*i2IfeVc2`wBAaot-^iBA+^&q*t;=X6I`L=)i&ufr_o{zL%dC|*F^`C8KU6n z0h&PNyqSE0Qslhu*6!bmMY=>cvy%Pp@mkkze)@?21)tbeS&c67U;^_>u@ z>lC#4y_}2cT=F%v=GS#adO^b@#OUUX#j|VmPAPn&HdQCX`xLLEZk7>1RI*B0-|SaI zK~Pw|IL#4ctBZ8??R%?;Iwl82ToH@!F%szRk7H#GMMQ4yBerGgG*O003Ju`ec2VC(uStX9PgLfUwD1+ zK=ABQwqxq#-ERGVmq)+TYPvohLX_)gOUCWzJz}^;UKaT(spNpZCeh!))FP~N_b;H7 zbHiR68Hy?Y0J|p4gV%Hq1*_6bJ~1zT7ujT9e*XYOlJJqUPpT%fBz*b^Ey*lFZcD7O zPeO4@Xy$D>Sd4tqTrR1fx$KG0TGx(i{;fuT;mFSLqqx(8l!rA{4Kza~^AEXgRcneEyJlg{d+%`7N@S=#hJ^WZ zKb8E{F1km}ul*F~DCoxwkVR4$g$+9kG?GPl%I=`JoJCJ32NFx+ZIVL^wo?Kj`@poX zjO})@erRy!GLPyTw6psN;ng8;XvOtY*y@_Ln`YTaqu1oBXr*8*w|(^_pq$}y%Jv_{ zUrl!1(JOsBx{%16MnWE_x^e3#7UKxXwD-H2GpU<{>XqXXWNS8M35c~x>K{=>enld+gqTJJ)E^|@5jB7Qazvx-$xewSqp+T0<&`ZuP1CzYh3df zn^cbFh1)ZH)qRM?mC3J|wfZ`+s)fa~X{Ya1-9#?(6qkR9RclWtS&R_+nrnYIL5dp| z^D>y#MR&RpB|AItbnH-^pCfu&YrUDBSeQNv_Y+}zd|$lX{GF25 zWwENB7C;bSxeC{AuVt@wJ;U(afCN2fx6!-O@KfxctH;MgdM0?|&1TBi&HJo}8q3Ol zDAb}Dgs;3$`;$1r`jbAl{1LdvYZhTuJK8p+FeSqdfRBu zs%&<(wnW$OMvS;C@^a*|?CbNf<@UIoRR1M0Lm3jRmZZejzK0Y<(uv@ zEme=thvGF8T*PW}p3r{k^q!s582+b}^^?JrKsiMDWI`D1T*y`qy-dQ+w`wM9IX#isuN=>Gu7 zgDpfqV5?6!^R(&TTgdu@Nqg^(iFmOcc?kKcO9}LoKz__B1MZTuX)!oz1WGOL{Vk*O zRKjOvuKgj*Lk^+ELoH=2H6#7xd7V^vx1n4PuX+}!pfmo?O`wnJnzP2ZWX%{G44U%G zpdw*8$UE%%cX4mxmA6X?eM48NlzT|y%vaD}BThN!sv)iT3)x7iEL#A?FoGbgqn@7L z^n1UwzMTZXLvc3K!DvnrhR#750sAC5h5oBP-%E?}=^m@pWVn}6@}L^-jD&hF_{KdL z;CQBEdN8MxUujPJJ$~xuo|*bGtmyC+MKhIc8+C|B+C}%4ZT_1$buOrylzNP1!|5pr z3qPr}5!xQSBM<}w(vnVXNc}G;@bwo6yu4~)L-Bfi_L`5c~B>i6O%5^D?O(5ka z%r|cfva0H&aWV-X^{u9>+EDsWJ9KdJ8j81T6p)axbGB~lI+tQG4)cls0F^*0UnAbl{_09o zJh4YBjrN27geXfAk_|PL7DOc%hD(>cj127WcOUvDm1M^mS(tu$t<1B;Hf}cI;Hla0 z6FEnSn*5Onbi0fH0M!E*VA1(sRQe}&;_C5bWQ}n-vx7hLbrys2M)4D{=pg){>e|wc!t~wM9g;aOSIShE;8_*c$uMW{>H?PNADFtIKLD#N>}(rn4}M z2ul-W;{IwQTOEq+Y|f@EcWxXITFBdvdK6b7EO|sMpxTwYkoiL^+T0YHFtJ|z6Kq12 z+=T9qpTHEdJiwdUTQ3!TQB&-|NSlqmN}Rwb%0tYCg37)!$t;rT`~1^At+;c0C57Ch z)i%j)_qs3?{s-|rpTQb|{1BK07>TTl6bfNjw0kFYWtKiEtYdw9mY9ULkMK^?v%rn- z9FNI$Y2vZYf9N@%W zN`Hjo^;l!tlA|y85`CZQl88qzgqt>J$lafQ$-R8UW$xv+=QP5e(Rsr?&V;7)j`Yki z)>D-+W&2FIqcu6h`e{uz&u1_Td@?RaioT-4E>E-5a!2bWfGNrCuwo+c=9;fj{Y0$O zqr2$wYI)1;K)&zu$xI-r8*aO5%YrEIw+0zwDX7oBkTVF z;v&oZ#0*w@NbGT)^IFZ#Zu7gfotT0AF5XV4To=+*Wy z0tq^&d$ac_;r1uDa*FBy0HN4%7Lv0G_BCB{gY;7U)y6Z^%|{_aI22Px?#2Y#=)S8A zuFN~FnY?(ao2Oj>qUl^^C|a8kjCUor>HIRReQ&33>W`77rq^xaM=`-6ohvF+1nK;M^UN8VbnvxlFfYxSw)kh{bo^>wGVL6BIurQsu;057s0J1@D^QSF$f! zPbp@0-C@n&#YIX-u`{<1tF5%wDH;bAJH#omPR7D8%Zwd1G97Fy-rTiUjG2gCbi;Fkh8ZHr7PzGWmhuF zJY*nNcFm?|{cBj#4npw&G^+*%RQTw{;sV2cDt=-$VeIeRx@*4Z6PaCoGp9K!5FCs;l85`qzTwVzA*S(T3f{{UCn zunuP4GrHEVgBs_TfOko;DC%;^05OYh<+kvu)aT8avt=Bw<(W#cIYTJ{bzGZz+sY+f zP9Ye`1SC8kzf>z3HK0^+mHB+sl9;w*mG`gJ@N_-{xZT?~cY`OAG?F-t#6&(zlE)z% z&kS`H&S4k(Pl@}H5{aryHe98z4i4ya7Iy14XSXeM@lvQDeVHAfD{ps}iJG9FacgbI zVT@R4zVS1Dv-4#y!>D1ThiGgiH~JZnOemUpc7P-z9`o}{R@E3umt^_9%Xg;ZwjG5V zatX_dXOLkQ5Ag(fqNMT$k{$jLZ_NRq5QT`OZH@dh7O6>QJmTXc{!Hrs0P6AsE_gxV z!{eSZY_A}{TU+S<2}~9m+&uGPwOc`t{_8i{qBJrChG3NAlTOZ;MFy-(X`Ex-x3mz; zBALYKO9h*Nbj~bH_A&udRRFfqn7{rtJtND$&8CZ%>uH9m#!ccNE!kszoUtnpqn$RA zDFTxd>ptvtAETG=d!&S4zhjT8B#$t-?vo>xlLm1p`lJKBeQ(-8t3U!mYd&ZCU19>y z84J5+i>votV@6n(O^L@b1l5yfz4+ZJ6+>mqm10-75&r;mIyqZC^8P&4?R=d|$~_=& z+#iqN>(sSdDYgA3;dwgES`bC8{F3&t&tfwhlgLE4 zz)XpYgJrkkwGwdF?wk#zZ+GRQZ8*M7(qjD8o8W~IE`(aA8M3vKaWi)%va`ArP+1NI zw={LV2#^t7{{R)>7G%8}2G-IOi0ZCT4o3T3Uyn3cm_dhmxuDf;mQ5c{NP!L5OHEZH z_AbpFd9(ij9ToM_Y+u$Q7kA$3pWbics8;;ckE6JcsZ``Qv~zC#(qx{lt??ew+p<_X zSe>2j-_6o;9#(Y139}8qpA@8|?fmgrjty{c@i3R`h?M9Q`J zH67>fPoo=iOvG)&nsO4g*TDty#O`BqE+5D-h%X>gN1V z*DNn&5rMWv%P*FWy|fq1`swq#CyZ|pWjNTlZ(mJQA{TAfA=BM{^ir6}LY5~!t1$D- zPsu(SskVa{OyoUH-q5;_jIDAx3CLS_jfqI;VPI{evt>Tdb9f@wz}XB0<(JimbXb0R z{LuUvKUGlTMpFhbx!wjNq?k}@`&G%6wXCR$?V4M|nW1lNG z((PwJpsHz4D~pr%ZuVpXu0p-_kB^m+3?7Z8kX_B=-fio1Q|@K*Q`*A9E(|aPA=?QC zosMbSRL3I~jk9KM&Yx8gt|n&5oyU${>pqJVHP+mWSiU0b56N9wxD&4knZ_nk3$wSh zxbU-GG^@fxYq62HWZxjadUHiK?Zw24HY{86#JavS^`FS zi!Hl8Dj^)(8TIPEh4WH^5G>i9_5T2UR67&v=-yu?=j6@!u1OV0?PEKO=a1>4irM(v zijh*(6s;LwH)O`@KU^CVCf6=!vLvOs6;azMA?Y^SJeS@;q$_D;?g^Y;?EA^RLq8t$ zCh`?85?EDg<&gGjb&`$5cCmC<$*XpE< z45~|iR!(4(b5)A4%4sOcgjEZ76l2%TC~eK1{{ZUT5)Z0+1E#K_qEUAAV;1%5am@Si z*h|fn&pW$Y@m%49SC$!5d%j5*K+)9&KxSuWy2sSjiDcf=U%ou~4hi*5OH}a2%!iZZ z`K9nQJ%RE+9Td)nqsM}P?VG(5rmD#Nc%j)0qV>K@rkFO&Nx!`k);!i1CvZ`EmuRCnSzW4A_D*CsljEwh>u-)#fNoHc+v*zm;o2|_) zUuJpjWxohOMc7EMl$k%mshUFHXCdtT(=d4|U4)yo+`E+54}hYVDZdfQIe6V^BQd@Q zut01*P|7|kA6`yxCQ;g3@=}VT^+vvS&%L{SY^w^Ir;v+z{=Mqbe*odj$?VQ=<@H2} zOC_0~Dj!P=%Hr2jsbMI>!cCR!=XEh$7hsLCkZjzWEB$owNx;K*%uV60w@XD3T!_v0 zKBkj57Rph!?bj{{GY~xTi!ko_SxM?-h$Gfg6W1WyM8=jKt7ZT)J#%ZS5sBuHE}!q> ztmT53%fQ@BOuDc4`6)2W?H-T0?%S7&1Bb@)?>_erD<{>`I2KUl@?|OTM5FK_B}p6J z&O-fPlCf}LDsN~&{Z@VE?^X5-3(qLrb9=gJcUGNr*&_;g@3j5wr81c31XB^LX-?+v zoAKAVN-*e*AqzH9ld5*@D5gUv9Qj0<-}PCQgii6poJpkWzm3ro+6GH=z;;6jgng$A zW-oF4Q4BINt72@~L(5aLhElWQ5@&aD79sY2`}@SLJU$kWEuG^xx6!#KV;plKX-h9` zPAQJ1!$nPl#l45U(9)KL<8@FYYa(CY?ZmT9;6HuC6n1!69_?ZHnS&LD1C!mLF z0?j6jEMGQFE18_`L-)JYvQ-iB8@7vMZ8#z_!Y1O;vyJWNs%AlxgSxT97CfBXR*^N% z=}8&+iryB~bnaDXlWMs1z0)vl-bl4eW*_H7AW7qW9Z=+IEcey4L?iHiDWObnrTEzt z5_b%?;dRA?B!$d$>nYbp@n>cbcTmjk+Oi=S>?9cnsWqSGfpvL7^ww>WebbZ4r+V6^ zmD`h{eIJ)?Yr3-iQ#5yL63Lsx!M|JFvWmLS+*--Z1#MAk%4Zkh$pja(`70>orf|tM zDZTW5D#ydmcZ#d%3o@}NbuK_#{VC%Bt{7HzMRshPZ&m1NbDdwz5AIG2n03mRQ~`;@#m?V)uVT6c+0KtlGCQ1aE#4O z_*r0VPZTPtO~sf)T)s#Vi9li``E+d)S2s4z)!zKmidbHtiQV7N;$^QMI51}0<%h6{ zUF0p-X*9{1xhWlENl7J&@yOr5RKpLvvN<{B$Q^56R!yrb8dAO7iMw6wi<|&!BP!WV z+b!6XIGMM-X4iW9{L^VFvN-73yZECggbNQNa$`N_>c7+Knv-W?yEeW$=$sWyhB_AOKbIA) z8*k<^-4D$Pu!#~9s+wu<6b<{2aBYW$rtfX2f znqlPdWov+rR_h?ulf2Rq3+}hmMCgQqGI4#^znk3!ryZ<TX^n|$^l7-=FH$-mgJ(e5@j37QIz{SBFFMCE<)v0#Tl6}kmC+s+0*@XR#$aZPG?niR`$or#|{8ZPD)k^00992K!6GGu?P7>R$Tmxikh;Ntb*jf zG9VM|OzmBv*#H202R9crX)!V#T|F|u|47EBu1=yVDhmID^?$&JrGIJ%0Opzhsq6p9 z{C_(|GBbBI1vd~Ld?Bx?lZzV|i-56`r<>D1I01~YOs$N~z_=KUnOwjP1ml^1_$L3x z2mfHpf8(cru#1M8H~;{N48~-Z|He%JV3U92fBHgZYUN@NzQzuWsqF3C!EN|g`llzz z<_?H~KfSq=bz+5h;sV*mi) zvH^g%@Q;tz!jF%)A^-qt1pw$t`A>Pr6tFn2!2E>&@ThVD0MuXrpt<`$Jd+Xtpam>r zye%hV7vq232MWB0w6FjGZp#3GPr3jACRo;p`u~6P|1EED-T%}NC|U*pG<*R7g-HM) zGYbHq0pAbR;A0&i4uFAzhK7cM0q7$dem)x2R^s0vh*)^c zzsq~;%tl$}5+;apR1#Z#k?rb2i@CJMy>+*z8#un=#|2a=R0;o+=!!>$-Y_A4^<111 zOrAeq;vMG9-7go6I>#(D!% zAzba{!7rh(Vf4c>1>CyeDPMV*O68D}{Zqa8`XFyXa`#1Gs$q=F#$!6> zaSPQ3H02*64Kr4NA%`t&zWL&SBBzTg?l1{R+9&+$zZANat68_>^x|%m?&u1?KGkZ>*iQ=W~91GrF90kfKsW<>0#S}oK`G{@)bur4_$;f-&Pg;nlnN9fek%bBJ(C6;JfO^hz$;gj)bH!{P7`MIpoon&MB%Iha+%Q!5iBV@=hmj6z z<2-2K?5S5FaEwbk7}OFr}F zCoWs0t;~kaCi_y4R?+;C6(F=YEl_FA5+KYw)s?hH!b-E5@lPr!;OhKSEA1X9x#w+A zX~@d-NXLEsg6}kXuwfbS`HXNi*{#4NTIGr5P>mdZBZGX*75nUpV~N;KtD+GV5VX*W zLGvR_8YV1Mt~5-KV*HFlcF96&QvVlM>0#WIZ~w;T$m#L3qhiS=UzZDJ=}q26gYIX7*J`E-r+ixo-`wUoWM$Wp~qG%4qQ@3Vb_~& za+tK4DY2H}-Ev!{V9@}sG+^zY1D8se)^^_TT4TOfkaP!1ccm4zN$c##NnSn*H7@9% z@+xjR`lQAq46hbSv$SV^Q%PE#CRmxp{1#YwS;0y;AWL?%+rIW&SX~%&Z>v1qPusBE zh6^iXxprhf2&^!Xs;3~u{~VKMD_zyq`r9H#W2SX32&qAg=ou}dlZF;e!no3*3v0qg zZ#64wm^MzVrb)h=QTBkjS>gM*Zj-NE{}B+Xkq?xo*`ONkChOJBFAGT_uuKrK`n%ke z!uQInI;Ny2rO;*>WJ8aw%P5Q0ZxQ6pF|aPC#zqxaYDru|{Jm0}E z$?aNOVg1ogLn!Pz>1HfB_KY>OD<@H?iC+EAX*V=PBr7SIh~i;!>04+EqbPU4JhwuFA?R+M309hAJzRVs_T2hZR=;p63Ux`J!FPbEJ=aVCH`rD#7#5 zw}yNaqeZ_tc!a4H^KO3=W_@#&9+B@s-@>g+(*1T*PiB@>{jd zUAMzgOKfw?nlS|G3Q^4wDq)j19_usZ0*~|-adf8Xc z0Md1O&fjb`mA>ky-Kv`(P~ z-PK7Pf0}BHQ}%_%NbgR1YDn4tNl>*jdI$AC-sefO-aJ}d5TbhZ24J!UNe1?OgoKp< z4R8=skx?<98Je|_&~;$4pdc_r$0uC>9?Ko$g0j7mp4@ zM|tZj_(B_IB0gLn06x?w?&`zt`*Fi@y`})xa&S}f z=H2KWav3zshaCEj*1$_WiIX>68EYwkVzI;+ETEbNnr$O4FNP&w=BzIbuoQ)KUL?yy zTr)3i(!V-Se?=s&b*q|BR-C={;ap42I&Jc0nqNy&d$(C-m@YfhIsjpPE3Z?cAShX- z2GQ0itP}qgPX8l(%Q}92&y4L|zDkI$``se!(zVL+v86j`Vs;bt6b}37HLaB3TA5st z&M)xI2ku>|X0#8;oru6_Q#&Q=G~d}Y^?v;4#~92{JUB`ddFi=DLHieTMN`B`Gaf6Z=oZ-ui9WLG1f@9w@_(sq9^eIu+?unx>~Wr)J?Kk z6Sb)6inlHO1zI*G&%pm{J#?$LC>zvW!kV#A5bhk81dUn@MW|+fsf4`>FoC16YE5vt z9XxFq2zqvPFG~NTd{+5>k(q|o{Z8O&v8_BXbtk~KD#-q>__@2m^?|FyFsk=%q3kLy zwM_74rh;0BZ*yLF{k(XN;W8;lKxu}rcYZU|oun+k3ESw4Z0QPcg{K<7@m6WHM#r+k z`j7}nW1(hezF8}Gp)&Hh9yhgZe%yS7uUawKLgzYrSnmC9I*&Ql54^$fl=GI6lLk-#E-LN~GJuB>5Uy(!{ALN9K<~7SJ^IgYL8|% zleeBS9F8*t-J>-ao_Sg`t5x5u#jnj^f=;K@&P2(_;R^Y^O=54bhhM%9lds?d3VkV! zc#zNsRtQ-?0H%^3fU^4Sh4A2tuD_gHsAaEt7XB;!B{55vy(B|@37(7zkH&B5v;>Ps zw5h5dQNFIwMPWK-`;!6w$q#3x?-kkUI)-SzT+Al4pVfGYs-+_9OOa!4GK%Wj&KKRO z2*1{ECB>tN5=GIie67)MHrdc|Ja?P!HNG=_8lzPc7Q+4%Xx$!S4_i{*CojKn-)igM zQF0~pV{R2!-MTuy(#n6Upku3%WH2o8O-!h)?Zj&zm6f)1S>{(&xwQLCaPy!2AWe^A z_Tl;U^E01v`>Z%asB4xowcDb=1}uuP`4?(^{Po6Fyj2ckVCJ1kq+PM{nTdXKQozbM zedIi5b#;6EgDp;H4LPKDDGub?D|vsVV&>&RS~{kp+(~%uDelXg$RyD;Q~jtOu!^L> z!Zj*2@}NmfmbPT&;)g;BjT$bw&?4#n0&2Je^g2^$;;w?esa1o`ecl(Tv9U4Fx7gSH zhiiXA^|oW$1>@DP_g$%R@o$!j67$2lSv-k3yEFS_zb*qxzTmw4AlhgkdN;B*{-rhG ztiGvsDDLUYtJ*$|k(AN~_mZ?7Vb-W%6(OlO7%|hHxLD@D$u~dzDB-yh(3xz0>vMIhtF&u zfF;{kPd&0bC%Mh+JEHd$H3^1(^dP5M&FJBH`x1x7jSiLK)INX@ius zKa{YqSRVj*-@%*VV#4K?#i>=|*C=aPx|%VM8i}&RApxHX-9i`qlNbN1Ka`i_79#v> ziltF@0%wMNiN6*N!1IdhcGP{GjE|cM^B2_uPD>Hx`Pv&D75NBYNLq*0 z$*mN9QCXy_)hj3Ts3Njc(Ybx8O}8#vd}-Upe34;BYqCAM)+(_x57%nK_*D~9R5sp@ zpj8wDqm{&vQOtEvuAq4)po_O>Kg>`rtdoyu%?5O&?5`GH34ir?3LkuD`}=DlfGQyC z{#~y+ca>_z3UEW^!iH4Jtk*-)AxoUi(FvwEc=-V5go}<7b_CbcdN_}H4hCH0Q zVwHv|I%b1mu|SrYdOx)z4JwV;a(N|?xp)Ol66k5^0Xry-<3jDP*|v17pX=bB3Qj3g zV;mSLTcUxhxI@_Q!Ovfx^%HcrWR^WRhmgF;GbE=xs5hYJ5U=Xtl4<5fKQqVF>|?;U z2hYT8&hjBf+zPaIv>iB#CzyTrsNl|!--5HIZ%M0@IU9Y@1;+Iv29318 zxNvB3f2y5e_|@3%H1(w@=nYTZ>PfjYPAUg7j9|1dkjvTaW*F^rgZXi;sCULs97_Je z&O)EKyn?bSh9HUxB_oZ?wvgs*0#|d&W`DYpJ@xV5Do%fzr(sKEPNyI6fsTMI5zVXm zxOsxhUb0kc;~4Sq6$W_#gYV9me!YzI3^uV(Vu8(Q77Uq~0t6I?v>~$Q@(+pJG+lFn z%HEB1JwNzcV8hW@Q_F-R|05N8(qyXNUAvzuqPcCG7j0S%YAi78cz!#=8ZdbwC6r$g zGPFeKeUl88_Ys${kPlNX?>G1W9AMHcFP|sYEuDAxH>iR)?ke2{pK_$`A4i?LtlZXD za^us=xg?Zvd0j~i^`M31H1308S7J}B!>_8Ixtug|ycrsGjaINmXn)3S5a-aO&A>jN zg*76jTI*(8Cgv)WKC>+%{{H&0wRi6Q0CsHaPyO=A4ke|!naWx+UmnbXFmblSr*y=E z4nGPk#!s>+wYVx;maAy(A34en1(+x-q(ei@n@U|$&Qy4kRob~!4$2DUyw<(a8Lr}? z&xVp6XuT%TY0P>P556Fury1}crd^boBeP}xj$^wD9#k7&A-2U2SQ|0S1duwG**-UX z<}R~fCV~`!_)2R2RcBoFQ}ZsUoh>nvpH_|bIwLJyotDQ1Jw^6{$SbL_8lIiXXlSb^ z!lf93rmaYnqCw7Q3N-|Ipg0~bWiNLbrK^*+UI!#l1E?pMre6{^Wl(u6oo{?d;M`;# z)uBgN)Af`a?W)ruxR$S2iQQ{K0V!RYl$h}GS@TjTbuHwrIpjSUSy&LL3C_p~FN~8G zQ)+%@*!f!cRBtBsvlM4=4Ec&lh;6e8JSzG!Tu<_emwfMN z!Dt~c!pE(g2uuWstw^nZ*S95C$k9yiy%^uFJ2yi;B{Nk+JWT%;@_i2GWDMcHs#jH3 zaQQedG~?JN%`h%?25(54nXqgcJw^rgV9c~%LDwmT62C0B2sg>v*QyJKu$pCO3FN3a ze^EHY0M#!RL)VqxXQa~|mrr`&X#8j%pchoVeJiv4H;JqCOVqvTZn=+!+&Ck;M??VH z-^`4v-OWy)xTn)X}#)~|3n*v_Q7FIiJz!rjr%eW4oP@uUQ@6j2(wROUvk4%m7obU$JlTGlwEskk= zssnT1#U-KdFYLmFP$dRShO+N(P4W#X=k)#j!sVWz2>{WG(0WdoDfIhrg=fx32HwRGJk z+bQBL&_gvTJrPwhqHFrt3`l*d6##gZ{r$@I_Bm+4$3BTaAyzMcGKDoGyl?!6{V)=X z3Z>ihS^5t3de<`A3SYYqBZ*mr-bBPax6x0wnBToZWT0n;`gcJh-DNvWXc@y?OqyMR z1N#^I-it3O>&#A?W%rbiZ%N-r2xRy+0ub81{zdnT5B&MeP;?%f%)_8;EP45@dGsU+ zUXOS2S-_J<>W3LA5=u??n&d>Pez=a(7E!5am{M+{FhTH}O=)RB<$lBkir#Xd|K;Za z<(_Bpryu6V*KnhnMDPXXDOQ49BKIoHivhWgHyl?{%0BNOfOCPnO3X&_yU?FP=f)f^ zKeKzZ_&lU0V>Lswl?7w`D`@^&Y()nLzjJ2gIUHYF_7B;8eTN!Bg|el}AoUZ=?3CpR zo!umf@~-=PHksArUFrWPJNaqiz2<9oBXe>b4^I$=`MM+&W#p>yD!Xe3ywinDey)n= zC+Qxs4IMYiv}Qhr`!m5`BISX+BAh*oKUQw9 z&D{e)M+AL_ist3i#IcUk5)(1i!w5`F8rN}8b!oEI!e2j zjze&?prVxgSV8cyK&V_wsn^1PxEQ#+4)IFfFYQl$RmIUG9ZoTt`WxKN8ln`x+10g? zy@+J0!Tl$`ahrgMhN$(LdpiP&B_OH8N0M-_`-mpNsmb{E<@#$dyuzv{%=+XYLEb`f zHF~S{M1frgkZxo|H^hHlhy-pzPHL>np-Z#$ONFz{hSY{{@@_J}i?QlU6O?25U-rL@ z<%zxq?q%KTzm~)I=-1uQ{Go{Abq1S{vT0p@{6bim>QgEuRm4X9L`d$~A*aAky->n& zX2{9mUqjn{q@%2DQMSH7X3fb5D--?N>Q5HD zuX+pFzDB&}A4+2rUFs(H>I&gX%dGnI&s*?s9z+a$NQDL)EQW1jM&&w1!U)hN*f|2< z03=QIc+dU-)K=7uEOYw4dvxiu3#5d&H~B1IU_aSOvdT6zy71JJTD7t76yC2GisSmq z&Y;e*1kMWMgfrg=9()a`_<~33*oFSJ{=9WeAEW)!L1AKxDIDy<><0Rz@|~@nRvsg> zupYUgUCjOTBE;z4%6q~HUu!M$u;dvG(}Ia3X7A93nBkwsrq{vz0kQJVtzzeA3@C9y9OuYy(`>+2XbLT>hX{Ehj{uIP*7&P87B^T|I-(r|(){ zQ?{k^=l|;9L|pU^=!gcfBhWk!;dN5lB{ZYIvw9};S-tUpSzMsfiqsEKR(9s|+co30 znsCWqG^+T9`EaIsc{U^b>53(OZMe~!x-8ZBZ4mPQZf&i%~Z%nYY`2G7BWX!~Yo?QExwxsb@+ z#RAR!#bOl=4`UphS5mOYi~$`02?=(QK|+8XX8&GuhJZweLc<`3CSw=HqyWOOi5X*2 zs&f3xQwH{_2}4{B2I&eS6&LOL$47nu=9zpcKLE*Bb!)?)CI$y*`DWvMlc+|$d{o7> z%JXR2u2g@m_BCg`WwI~W_0?*a%h5Ty3gS%3Ocr7)OonxjHzS29acx73X$)*rmC+7H zDSFW+sqLofc~i!Yq|vltvUiP)6W7-e%$FH(puh;)bFlD4Ij}Y{k`*=PlKWnO7|G*C zdL>d6^KICjPR?*CBs#4FzoA4|yaR>RPamay{;VZ*Ud#8f53Q3bzvZKTT0K$0g%ylw z2=ZFXEs!g<{8KcCdy~t=G?_^)<>yQN=20f*dx|EvnUsQ@6zjz6l$XxBCv~2Tv0<5H zq`)52Nvy@&#}Lzv6EDLF%BUA(qs<)CGV?8}9_ zJNxC}Qz25Y2?-~a)P;U+B3&b6A#>zgBzGoLY{Ri#G*jH?11DK;vx26Av4od^iw^+N z6I{fKsUO<44_!Dsh+EgpEPxRx*|5(|=taq3QYszKlyaE_YUbvajl4C*DvwiDWEWW% z&*CX^!J2JDF~w|4lmM+{N4Be7$9O&5PA44LdWhOus=w6tJy4GdFY>27 z>dqad={O`b%E-muSfyNqClRF<0uPGCct)jotda}3D9u4qL+ttGZrc=F;o%N6Q$b(O zBqT!RM{Ymc2IDAJ$D~A2CsMe$SSnv6QTEqgINGvLt>RE+(s2r(E8i4R&0kBC465oG z)mUZB0Oo4y?iXq(C;px1@d~j!Cq#36cbMIp-l5h|2~}RxcE_+Ubsau`U7XlI%tiF)2{|b2lhkigpp+kdp z7p%EtFcii>Rah)aHgRWc&cJ`EFXTV!`=Dx0kYfKLvlxFdvb@-50Sy9y)ZhQ&?$k5s zfcQC&UlJppqFlG~2K9g?%pCe)BQ6wIGZklyu>nc)S4{`wtj59Rs`RPqeENk-QZTh< z=lz<$(uk(Aqn!t~3Aw|lb}B%%j7gd|fA zChaF`hL+Wrhxp9C5>_UGWJV5chf zDp5`~Jq@R>Kr{9{+qa!@S#J$x9!K!$>TD3Nu!W;6`mG>OzQtygKu2&yk!b zCB-YY=Hi6F`kcc=4r)UGSzOa{UH@*)n{L3afZi<~^-=k#Mv6ss#6kdwKbARB)=Q*rUdU~L8CPuOM$K0be5H|sjYhLsrRtzc zmuRLRGQ@O|r03&t3HMLdHVrAhV(^d>KsqXGVHAWV>{nJl%#$`XdZ7MdICE$#JOX}% zxJW04RMc2o!&Wi;qU=!=3RQp47Ou*6n1Anot~9)kx{c+r(5x(iBmZ?~Ft=~yo9gxD zqJ}uh0BgYz*M5T8Q%m4hA`wHkPm8XGaZXdsLlDH0u{e{u8AA;tmX6^%`tXTS>ZawOf;g!yyab-!+7HoV3*#apt#FKHB1La3mSwv}Nmo5ty^Wyy zDF>L+S-*?2L|w?Je^R8zMOlF=t0}s5(CKV&Ju;TF+XgR6Th{Q-9af`-u_96L(uTL0 zHEB_JI9fA$DP5QUl`m=WQKtfa4wF7OM^wNHmo?@r|M~O24Ga zWRR%HB2bVw8L*PggXPCy*g0ky{i~@h&;pW&-eFKAa4SI1Y+y>H-!|i2iOG@Fqp)gc z5!~eT^T6MutJnxbOk$W<@z5QqAU}_(9#BYIPoK(>m1*O$kZ>`{;#yA>h-Tfr@Qf-8 zGZ2Df1x0*uj*~dM{8^}@t3;n;j_quxd7|K(hSA2mPD@JyKPgW)I=G3)ns7I#c2S|G zxZ!RlS7aVhPzYNfW5YC;rp+MStTE>oDH*Gq{6)PW4ePP_3vtx7=60C8b8(&)MI=a` z`kOeW`7dV|m)m09q=Q4Vnet=kVk%$F&DME?5^ zfO<~Hm~aL9RH(LGVIod(ZT*G$IJ^liB`GvgI_79Rbpv}wJj~}d|9nF5OhGu-ub3t8 zLs>7Tk26Jy7j|KCL*kQuK8NX)R;*T8Ij^P*9E|p1k#bMk#KCkXWTHF(5l|NR`gu@F zGzyUz3D?}IGAEPca8&>`?gNHAE@LC->v<)5_yjTbG zFVo|n=r(k6Gz@l8XiPE+psKMm8%JOwrHGn|3zk>`XHYMdcoJxK_rKQcV1*%X%KG>V zb#aWMUiUjTy~8*8o#E3pl-lmD3PmI4z0_=Dx6c0`+ZmF4?%*VQ!GHq`k8kb&ZL3+i zfqP`-etHdA+@!$|U(jH@-XhsDJ(NADkWjophgp!4ih!13ICG7oDZlut4b_z@7P!fV z`h!-nlXqn#akbjPn4b#Irv~!2*_}{5q*F6EHI_y20|Dni$r8H2y%55L9wcE z|FRZ^95&2*uQ$m)M=GO;JX2Cn0uUgdlIE zr!hx5f~21o8D8xhMGHxnzudXfzLWHg#^4}tO9b>+j>tj7UcQwd!|#U*MDcq~AWw4F zquQP@!evefMl6d9_3vI9UOGQY2=F)y)c3>xcqJe7sd!BvuyFn!C@aZ}>`bxP+H_Jv z*&7we8U#`j2ddC(EDbvFF{r@!a?yaMz$1LrP0VSQ7rNI*Oa`yq6>J+Htp)ecU*+lY=YSv&nso|u!mgB<3b69@JzdJ=QkDe@RC z$*VogK|em(Q5MA<%^XfW@z;*0>!MBU#Zj>(_Q^t&yz8P)dkH=xO5eilW0r>VhB_{m8+&vFu%+M&Q>P=~!p9$d(5@q-Ew=>-P@;;!QdH zh0H#v()VDaI+O@8cYX(Y{}GxG5S7k~CJJe9UFUYkgnki3F7dZnK~{@0al0KeM`1nP zmOb;N5}sHK!oB$eK&@{ZAwra*hp63S)A}&wpqvKT^8wg}Lq-kt9x9F1?{k+NA_~4{ z?9fPFP#(8{`UD?_7OHp~Cua})o2@d^U7C8)*!pMX56wmFqDQtT2MHMhix3&7 z!wKDTPmd@4vfcVv(NB0|;+!P5RX~P2Hu_0bg^JG+3NJZm2rHwFiZ)YK!W#HB8q|Y| z%1D4z$h>~_@1>{L_?ukERI;OZNmb=D@dtG-BnYN9#~}ixNaXLHIg#wRTMcr$H52z)onYS~oTkO(>h;0;h$ggzsgO;grmD zh%@8X2%d#HmtH@zVd~O6ixpSD^9phOVP@|o(r@UlI_p^-EgGW4>5+Zn(9TJf8QL~J zv|;U-?q*PvwoNe+J!T@{;EE*SEQ~z-vfaV7Rfc!8byTk!#|uas%I!la;dlBx%Gkh? z2KbmELYAnDpJ%GRXUF&{vO0F1w03;?20$#K?H6ls*2yIvB{|&Wf%2F%p=qcSY-7Rf8V=0e@Ux56Am4^d#iGj5X$BcBOdYk z!;L^+Ni!2&&9ZMJeJZ}lZY2RnS`@%Z-Gkkm#pagw8<>2O1yyGcLvGfL zaFPKTnH-Tms!$#RUQ|?UwJ?}zS_ZG9L>F&W^T?gWqMXaO*O2jQR_VZ?V*;6aC5i2m zP`XPMv3L_1IZ3vN7LRh2M)Hwo9DXWyB`H#}|CBC7p@>D&L>jm7hsc;THv3Hv$AD8Y z;#&*lc)H@bXF5YsfUk9;a&NL}gldh4U3|_Ms^M}BaHwHq2S(U4lwoOIlo!5hIWqSI ztDqAVY|w2gvGazCt_;dT;&;YUQxP3w5ra-*h>139@1CE8Ixr-;~x$nlSn-3djp&Zh1>|!vrmTKm6ww_ z`6${`wGodrjkeu{C@nyU59FHi0$)%_(To3*burVA<|bDY5OIWlF;+26^sd21q^ZST zN|tz%z;EMQbF0O1NS?GtofS;FT-eES*vTfvqJTBbR8&_}B@4s!O+ts}QRS2hGF$LR zuu|aTrLU+!k7AC7?@E4-WnhCtS`oWrAhwO%XOHN zph25WJ>p#h>hrA3K$B_*j>YIC^(CW~Nv*@}TV+zi|-+^of;&N%GMu1Q9R{)!Q=u745#ZeP;Rodnpajbg?gfzh9-jpP^774 z>{kD(sxb78_O_}JYaFvK7{F`(1Xk zQb(EZ?0E9G_P>T>{Mn<(LYNfM7Mo7N`7FeqEPP({#_9h8!~f2cSx0);(vw&Xkkb@>_PDmD5>t0VeVX z0O?zU;{sD9nOX6nWN4#gNVQh->ZdkLD#9u%=#yX>X5+U&H?_vjOxDHzT;jjv=r#5a zHwYTo9S0Jeugc6_rl-)P?(T&mU$JCZE6hceu0oSW;bKrWPdHC%6DjgHF}R!hU#sT= zuuv8^N$|ZzUo`Z$O*My*ib`5;Ngpj9>)UzJP1mLA63JT=US#oq@)C(o!oRS&7QFA? zGpu_c1Ao7$&kb)lLU9^xeIu24sa(=M)9$Bjv35syuLS>zge#S%pP=$VNK1Ggk ztpWKh6wY=ZeJt8jy3@P6QUtaop;^Y&V>F1ooiaNn{l4}Cko2CqeJeS0M6^YdQv+9- z#gKt%S?GTFIo3~z*XyBa+6uLk%AziMvU*`(f>O%jvk!S?&{CdDT`+l80-{SLJZEST zUk=Y%*wDEa2e)|mXa|d#RygONU+*6&Dnk-X9d>G)duA;5U*QK`*A z>&R6bcfC|d*6n~VYcqYh#`+@4{VAel7&AJ{JG3$w7D{^0_HSX|2#~^n_5+}yb7hyY z>)z!-!BaiUASc@k&E0fSWMaC>#uc&!RJr%~Iy5%>6LuYyt7r(5&0!p*!sXr}*mjUL zUD2T$zuu%ObRO6}9@ssWjd^TbZ}q987RgJE*w3RQQRH($f8|El(gTuY0MEG9+tiI{ zeHxY^9D?Q+AANX9jOW~QXmCw?^o*`d&4HS;F>?z2RYNl zE9wGvn+McpC0zV4kvgez7fO9O{Pa4S>Z$A^=r_dL_M5zfin$&jot52D2@9#=xClFx z)8U#x;)=_$;UfHf7ZT#XB!zu)y-rM!q%RG+m0W7L*R9%>bI`}F!ehB{M15%k(k}J| zXvk6JSqE*aoF_>=jN1Ufsm;bIpRF-Lz+&m>YL!)0lpXhBeArd##6^{Ky+DESPn-;) zrP4NNUkYwYe*v`3NkyXc)51;(+}hvFJLOg}JYQ8K3;m=_W5pt0!ZnXkWJU;Se_oPr zX77$%e2Ub=N#JWF3tLJZf&BpFe)nea5PaL+Iw7%1BOFsSrmgIgN!ly5@0f&*feYsn zpsJJlHq#9)qGW36=JUhg5QD5I($43?`nU?R!y2KhIu!TJt}7X3AhG@nxzmf@4TdLe zJ5Jn**OxlGixRDycAS&$L&cfcV~mh6b0lSVuN}zPVot~-L`80-{2O@8gdbV+wF#2f z5sjggi6Bse=?qcxbU-2sY!yO;8791LgN zgNA{VU)Eo#F!LyLqSgo z!Ya2%fkLAPTr*I4dw9+eq<_#s10fElJRPAWaY3sq!>*u7zEA@6al3II#^D3={J@j! zRi#z24*DhQ^wtu6loLIbS0ul=Vh=cMf1~;7pJVy9+tnL7q8|WDwz<-@55VobhRQlV*yoip zfILC|0R273lsxS9b;U5Dw^TMb2AbV99Rbu%&9g})MBoeeYFr3zeeXj(bAnxw zXs#D~tw&2$M#V-J+M^h>Ot>6O?LF;Zsl|uegzmhL$j~*#5x$=~$Gshqs~8PI+bow8 zoI#75rerS~KpBxv&;*>9<`2Iku4Ux$0fUK?X5D0ZN0ii%VK!mSYs10+Z&_4>covJS zCc+OQg_^Y=O0v9J>^H6G&ZS2Vw)WFdKb+5)8R_X2X(YIhsaOPtw270Np4De4W0!s_ zljcpT=ySKA6f$6ps!C%bW*(w26kFrM9$i69+YO5b5r|DsMLWM*= zv8!#h+reeZJ#`=E%L;_Dp3ez(l@Aee=ZWG-kSzftvbhuJVRq83w*8Dj#B9yyC+4X`wdP5*+H4}9JYnn~LRflm= zIu>XY&(8ispBd2?{z{;%vG}}h)1>5GQV_iE#MJwZhT&l%Gt3^8$PhPNg0tTw+BsB{ z`Q5E)?y7wPJJEx(%iKG)N`N&oP?LB7L1c&>-KfX?6TPJN~%52suwETgSOHa7^+B(I8Ahk+WO2viN3=Ksntf&Pg zXM_c5b^7Hy(#GXqJ#rdjbJh%+V)oQKl?qvYWb?B6RnbW<)y-X;E$qB*P%fuSY-2Ic z@k`VOg#*hE;xqkBf3QMyr9}}YRHKl%%PF0Ii)jq%!;+4#PS1qs{$(xdiofzt+$5H| z9l#NlcjCeUhD4NiCIINDclR`gY+x!NPRv2i^36rG{S1huHg`ZF=_1sqh!S|I$cT9j zb11Glv8gM3FGoF6i6(aa1*zNZPQ4NST4$VN<_RuR`lGmQHB=G4+)V%dO_C1)Ov>=B zCw_=RwY;*Rw8~pZY&1klIJE{yN+&|J*@zmA_eA`!*g`jDAKF4Z%OJX%wwhxwz{=o; z1fjS>qGnKMqQPQg%)%D9pR=r1uVsRoAW?1!ya|f+9p-b`e=1x-gO{i1m3vc(6m8aN zQQmLmJ7mYqnyDg=jAKI}*`7dY%7-i&uV!DF5ZwB~dBH&2VIH2Cnpm!Cw}JLk#vS-m zj8Sj(I~^m+6y+C12}>?>prJEZT65w>p$16K}Cpxs&+9GRann9oLraUuzRZ7(@ z0C5buze}JWQ*rg;n~~Bv6fi0ld((^y_adiG6RZ0%cION#FC!{p(V3mNXtC7+kBJ@R z$LIhab~J~t6HY;AOE(0S*!mAoaRFqGhm7fA+H~=yx=|_*KN6}^xJFM$GvKi-$xe3e zA@`AvAj8piXu-n^^hh@*`C=qx zcsFy}>m@%FTOD)<#7c#@I4L|9-I;+Kt4kN%IR_v|K6)26BB2+gdqX;kec1Yln;PSh zE3aIrgLmNMqx)U0B@cDW1oEebcPTMCjYz|=s(<-Icpe!yPB!PH92trIcl*pxQNv4k%KFwk_EBul=KcWjkcCxHiCJ4^28B@#f-Ef-t5Gn9}qtqU11J-$eW=oEN*KC zx5%&#v`n3VAqCF=tE;yRimU6QbsKkgw_uIC1cJM}ySux)YmkNj!GpUr?hv4H3GNag zI0Pq`_q%oKRGpuD)$Xq9ReN^rImZ~!8q`x`DX_eu6%OVkyN*;jk@+2NaAX}Qt$=a5 z2NpkucGsOUdA7u-XhB;97g1H>M1z{~XO`s667|CL(ESf*R1gCl)$m+B#Wld_R)d}eTo44S_hp1c$X(huesd{US}AHht7Iz2*L|U6);hW0+B!`!jhgUs zy1!Jw{iS%ws+{8#;X+AN-i4XtKK1EdG;{JplC{1WayXm$b5$0|T-U3?Y_Dks;-x$_ zniozZgQO>VLWmv{z7IdT+($P2n%>9Mx*tN>Rt7hXOYa&>t7m*e4Ullv?xdbGto4(K z!K0x-f67ors`tAk2Hgj7M>eJ-kK#a2&y%__wPD?mZxC}bT}aqkI$BQm@a}&H z-lII%nnrYJL8Euiz~kySvB{Dq{y^pZVtQ$fNicK?9W>Q4vWw`(u`%QRI_bmQ6#+r! z!V=JP&k~c;>XTU}ph}y$X}X1*=*;$IdKkOJ-r&gQs9l_K6`EuxKv%^i)|Y_jq=ctj z=$sQ&tVQU>TR&0CRWCpml2{czNZd)vO|U^aA!{bowQBG|$R8Hl*UnweAIWGq*x5R8q^3^l6SLUQK;mZuGIVU2 zu%=QU=q~>U@Kw5^@*Kkck=UXSQA`t%;A8zBIZRG|P4T%FG-O%Y1ROHWR(;?~Jawgx zEP$JVcX7R*_D&Bwhdl&^881`{WJ1b8aXU#Jxf3AdG7fQlmu|f@c1;WRu$-_;L#)6V zR>a^&9?JLp=`h@p?pX=3edE*$trT(#=8dwqZ60`zJw>cmlkCR{TEd1z4@O-|*~Z71 z_PN&~IAQp31NITm>%Z9=XL0qNI*zMXLw<^6ie>XRS7OaH5DSfZ$A%p{s@4OX(1=(B ztXNuZ$)7o9ckT=Ihipf@f`&OkKv{wPYzIKMb0P(Yf9+KOGd*GkCJbkmp`p#aDSwt$ z?id}g;qZ8@g?P3Sn+S74m7<$#5d}*YW262H?KP_ph^fP!k2R)inAGea;2Q)$0)P&; zVn@OAa&h^qiN=cb7&DF^tJqdu0nMMhZ><5!G+x0D{5quiCujgUNFz3Pk7qL;czB-S z4?QVp156ao<2WkEbRW8P$SgU6a>v@D;-PDjIx4H?Xa_Wn0@iqq-y|sW5V!0+WXBwb z94veilBa`tpC=EG)jKw!8LF(It@RsPhq!kws&$+st}AWZ8Q+$;1~%UFgNmWeiVJfl zavJsF^oK$i$1sY*zj6caMaKRCuvB*c0Wg`=+o&E$^Azg97AdMEI?O-;XhCCbyU+v+ z4gID)x*j`eG=by0tV7gTYi^Cy>3D#>fH~+k)W1%|jdDMBZY zQu>pctjTVYJ?MG9f10v+R;~f3m(ucjN?ubXkdo64D0$7VEJ!HOWg6|c_r>pbeBa(o zA>pa@JfTLVJxR}?PA?_1(#VxU;}qu|uf#nftOD7H^!nr39tkLa+Ex*fOhzIw$9$bs zD*Bt%F;PkrFT6$aPE<|}q(}wp*9uKr4=#WeJxn#5l-6~7wuE#qZgvKC9p*`L2s@}u zdgCXEE)&yoe>riQIi9ajjBg}+XzhW$O?8^W{G&tq$^~_TfhLrx8BE%cbyf|r`jYk< zxs=@<7z_h+e7(OV&A`SoWg5TC`W%(cX{CC^kL0JOKV^@L96`Z(in=$cDBW2oV>V^5 z9qv&ZpCm3^m*PwhmAqx18l#2_Us8YTD8KWIZ&Vs|~1>16L5YM^pMdzD7Dp6s_Ud=b;caUN6$J{Y|EC(ws;;+IQ)IGzbLb~#AWIrHYysw%dt%6g5WIhIxP>QHMGq95L zzUwWcTW0R8VHJ1sPVItkp=ZvjSNd2U5Q2;tOJ4U`DUU(Ll@a7fj+XpcRKFUMQ)D{lP%qJ zh-hxVG|HYC!?PrM;Zq97k)yb=H?_N`c(Xb|gvu6EK?1a3LWQ{Pd#P(Ex`>cCd%jVr zS{y|tZKtz6*kU~#8J>9jHy%CvqM5oXO3r4E;pu0sPEq*vr?dU*W0k{1&oq-RJayr_ zTEz(M!CdxXu1O3Z_fg(q-&}9y!fkiWT_Nr_bjpJ}`1-CB(97d$)?D7Ly0YfpUbupvVZa++cQ_OxVVc(EkV z*6_K~qCvW7kxEHS4qkhYc+Ob^i!wY{D!nJ7ys*Vhd`Q&Tr;G3h4T#PMv^F6KYr^zb z6R{ivEuV8mq)Y};co>{Ibi{FRAe9R#E1>DCC=YXwF3^ZD$P^30R+Iu2o9WfguZV*P zOJCWHMvROxX^x385uBc-8qepN`j`y^S)HvHIRD<4^OmQJ|CCdWC;W3MuXzA z$yCeY@g}JouHI=oo@RY*C3L5&JTh_^htPTs+C7a{=0>1%S7!YkEg&*b*(3J7ivIu= zoFU!rp~b)w0ey{?$x7iDrTqQn1W&(MyJ(0vXpcFGG7tB1b?o{Jw6cJQ4w4!$5kH|) zQ|@*V#>O>BM1Vm$-4js4j`Wwa&{z5A`DosDHGV{c)sx*y1XC|@hPkp0My#nph+deO zHIcqTusynHhvz6$X^S4_pGb6=zrl0-WV(x5VN<0GW>QjmhG&?bgj;z7-4VeSL&Mx< z7Q;mmaT91Wuf7>ni-$a{8~yp|ajwn~0+>W^cAf0w7YzV&cUe>9nGYxAX1Tn9sxp6S zr!jHy3FkgJnu@p@k#7S>dv7gYwz zxrNtgP9yeQ%HnUqHDzk$O9V)`-;~U4wo;a1J&@$7{X(@yV_Mbo!vI>qG^pURiAiw9 zMlkkFBCR^FCT$CgsmN8%hDud2`eXD7R5oc{bMQ>3hCK z$+Jq|cCtBgOma&7j!f*9U;_Y{C`Ibsscj7kK2lbE%pK;L6ip%~YkKDbf>i1HtK&#RQg%m#v7p6d)$*A`~ZNoNSY+wBYOzn`{P>+WoImi*!5J2qH-8zC( z2k=Nn9sqCky>xF&#PUjrlL~PZ^T~zW9FC&3MH(HN2^ z32)^^S0cYNx9px3z*48Q)UgOM2mb&cQ|A9s%>D;(!$hPEN2Emi$d*t{h=p*iN=6ob zT!$mlkL40%ss=pug2F>)p#OJMFPJrq;PTL`jMnawC-T3KMbJox!OX9_#PXpszP>$w ztc3A!R7HuDzyIKJ>HdD}CX7lM-eCf$)3f?J;WS^l@OhBwy&+u6l{By{c-i**_DqRZ zA6_MsIP&kae%1++Z?6_7vz^L9a5y4J08)OLkN#Wy%i1Ch3)SsKJ1HZSJUD!l0 zxvxgrO5?AI_)8=|OVmX_g4w+V+va_?VJ@D+-k>08!x%*4#MzsIsml1Rpia|A?buEz zK?_)0jH<)o!x_2d@ucE%o(sg!RktW)V{T<8R%FY-W6)FL6=eqnHB2tA+BcTSE+b{x zq;xjQa7CY~UgverSw&7jMd~*{a_&Jy7A#`w1N83h{ElZpt*(Md#K)e#f+IM_J>A3F zss!=wY(IUhAnVHWQ@&?}UOa~C^G)o3Y#-vMwyd($O8B^xDt+iLi?ILgdpPXX-%9r< z=xsE!XS2HhtsqiS66V`u(yARy~`h#Zozs$k^R0qrt*@4oH zJ*=p4ckX|J!+&XmkF?w!hDXob+t!`U>G6imDXQwJV-vz(Y`xJ(diq_u5DbJ<0rI3Y z6?OZ2&%v5axf$X3V^csu$b7&IUptUbQcEFrlyH7^#pIiuLLvYvNoz>ZR7&=P2@h_mw;C_C{%=s!mkMeWYrFB zQp0DT6Mrtvxbg@DXA@mfz-|Q&eWwSzKb`*#nHk%36yMw2<(s27EeIRFV`Vk>H(;l1 zD>!(K5On0Vn!AocG48?gcHZz?|9bxqAQZhm=eQ9m&9tL?cVX~gNlAE+tppxFvTWL* zcJp|!-0sroEU$$msV6iz&PKqe0IaxVB*XyEEA z>W>|I8Ghlr8mQ*RhLy6VBPX)a<&e=ZYd!h3Znu}!orZyz%v@#XYyQH}4%* z%y&Y?_hEM`x62&FpqZG{NyAln*~fw(b-Jh*L}OTAS`tmT_PDnn{sF$n&CJ27<|*FV zA%BZqRkFb@2^f`1mAbbcU0p{(fEs(1mb>BomG8~)!0sI(qju5zI$R?LvXN7L&ApuGa^~A_7|B+0ERAc;P~I zzb5x>B;U+>YQMt?df0iMjStAnMxi`5sI{{0-ICAK8q9 zHVqtoX_`)Cm<3*0ej-6=(@$=?23_@z{^a`KKcY)C71Un0+uR3$E6$h`G}Go6ce_yN zWB2!mFhaf_v(8@_*ETI!UsC!NT=*4`xRSqm7wrw@;P>S;rfBO4;p{n2bPtZDd550} zIp$mtaV0p4gjopFUcDL74ju~N%NsuN*bMUxr)+edy|Q6#mUhm>IV}$W)bS0<*0j3o zU1G@vr%xV3J_?#1>%Vda!2QSE`F{%9jWVopplELQiaqUrH4Xh=P2W>?4ui*)?Va(Y z_dPQaI}qGevDQK8>0MO8X-23_5^Bw$B2R3><|I`k8no+3-I zo%J!DncIipVJkDdSJx#6$Rh_RJYBPmcV2`a!S^!=G#7N}iewI1cE8C&h<@lCud}G+5{JI(YZRh1Up=StYOA3@Wb#5Mh%v& z>X0a~rU9#_3~vj3p)4uqV>z5Y>jq*ddTg1GOzK5TPIW^{JH}uAjmAHfA?Oql{lyTO z0ZrqP{XJPm9SF?v$SxISrgo+S6dpq}SjpGfrtqyD0_ZqM5SNZrZ8I%DQ>`M^lp6FL zb@Z(Xu@3*CIr5>o8sH#~- z;!;T}epYUC@-Xd$4$?XAAomho>K+nWjGJY2L+OSZnRB^GC00=v)9B}e5<$6nRSz6! zrR_Bq@SLwDzh@YR4NfNT?97_6mkvzHq{5h;BDwoNOMSsJCZ`{O*HkjLHQT)mEf6Q_#zgN24aPX0cI zes2VTtwEZgpviBn69Tpl@+{oQ(Hl=ELiuNu7y# ziR2=^;gesv^1tt%V4Irj+rhx7QHgm75FipeYtfP^Nq)3;;hMchkMGY-mw( zhkb}GqSM1u7ns7U6iQuZ6MyOPN`IKp7BelYTu?>&e0v#}LP=T4hp9)|r2?{5rDTUx zx>b*M^Cpf5vS(=4-vLh`MZ1$ZF#)*- zv0fj&7#+{|d7PMlgA{HtT7u1~(-z+M2f{|%hYoRv>>nm_{S8H$!zhRYLT=hh>xdCs zPF$TT?&pv|mE8&h)fmOq*1o2nA+B0VQjQJ|%e3U#H?1@~;Y+j+qIF4}Nnv0-IAklA z+Va5olFX<+LU?dov}^qWMLHFaf!oqM8e~v6!4ZO3n!~Kj{fW-orU9*<-YNg%&0tg& z4S}hMNH05^%yf|6TrZM~b#Ud&S6?%6!?U72L<2ZFrX{9j_SU{V91KnWc>G1Jf$Xf_ zft{qpfqkLPLLml3K{rM$CVEr}p2FHoH9j+>82n*_8i^RWUqX`iUo%j7NJRz#lI-4j zArbG;mw2!Ogi5iqvpfNs8maf2Nj$_RL!|r;#(EVEfQBTT- zgsHpu9%!_^a`IpxhMx1dhr!SqjC)_+M#zw@wBu`fNfdncxQ=pi_pg^rOQc!O(UwV$ zUnPGZ^m^RI{RX49kZTy;5tS<#*uSmrF&C=Egqo3OlkxGKngg&y84^bX>dB6A`>0$Z zO3l}S&1fvVC(LSn!?@BH=10W?jE%AIG0LW=Zf<)sXgW5*sX3~D%184!++-CbhG7@= z(b{)@e@yZa(?o`Jazg2Qw5S-UU`H-3`>(&6@$GV5zW-^k?`XL>yf!jNi%e+vvM6zt zRq0{HV&bc<;naV(RcXTomx7i*iYw~JqL9LCTpz;T;J1&&XbpE`gwFCRPZ^)+;}u@Z zA0+F@$7yb~Z7UXmL0Z4cfPdX;bV~QzxQ607Vx@;qQJgQjoUpz8ncoVeL+Ey-U)|$V z-NL^T$`rdPd;PU9En1io9*i>%ZB0k5)R$(qg?B1&(f=irohdE4MOu-KEX9OfvYGyc z=nd1Of^0dq9@{55Y=z@Q*bG(|Q7<54uB)8NWNnfQ3lol#6`y?h(RpY!OM4Ff7MC>? z^f^nN!TGnV(1bdAW*d3&&OQNxw4uiy)4>2vVlcgIH7k3?AbzasV-(a^nU8=Z1$%iC zA5p?6rRk}Xf)7uR31-ebc>^_~Tvk{+1T_Q$ap1>O(O>x$nN@QSE7!s2WS2zu&FE-W%!k$s~2X8`LYxtgN3ZNIUIV_ayq^uj_1(t^SsV ziO_emccTLOf@HRY*67t*d*~$wuwR;eN%iVlN6LsYj1&-A@`yg*+4SUtGT<-ka5gVa z;s>W*rh2F<*1)V8nGgp^Pwn4KTK~+AStJglPv_h}G?jMDj^U=MMs&tckUzKSM!vt& z?qEyN4?`(1?`nSm^()*kG4%P_l3_w^7vV2Jyl;L>!#I(fvPc=gY##;{i<(EQs@iXb z0nW7;6)>8Nr1aRlC}Im6`ur*S?6su4sa*;;Qh%EWj~yeI+1@yKLe4qz6N?06_0eJXE`PeN6c&C68kl* z^bRDJtLZkq-297uj*Xb4)}(vUAHGuCnT?tHd91@LoqBr$`}QPxwaYz!05Mz$YX-IS z_Q^va^$S$I4W2iwppo5&e~C~XAv*h)@ij~ZV#~1x`#|a?@n^{&STLo!R2rA6CBC43 zZ=>G%vC@2L3WtsoHAXah@iq%HaNNv{eMf+#7-0FnFzZXER+mLoJ!bz-HW_0kmY{pd zWLLd!;6!TmYBVe`is$wtNsQP(MX@1?!b;saYfraq#{>^W6%{kDwz6Iaj*#4p=pGX- z&m1{bO@Z#zjDhpf252*eYkhLt;u(S)y~Mswxnl7D9y|0+_{^M9(YschwE7jAtTTrD z(H0b$dWqoXmk7HIALsZI5b_Njs0nR*4o8mBp*yF*Sdjh~(>5Fxg}Lmi_+*)uF2gkk zk1s(ZxlY8JvL^ z*1Ff+j>);xg5%vr#~%ZPA3ok+GsnI0J6<-)P~*x; zJUCmPV*E$3WP`>jcwbU%y6F@!O*G$ORT(ELrYG-_a$N-7hodX|ET!<1w7A&FR;$ox z&g(Ex{1d2ObqFB>GZe|m$mX*R65|95--1i6)`z3K8&+`N<;h=a3BR@Q$Z`j4&tYmW zWKwPZZp zevYK8TE4t$KwGk?F}Pw2eSuirDg5Nle}j2?KuxM)tH{cfnq2pY4qnlaXOb|cj@0OF zIi(a{uv^f|mwJs$=N4E6S;g`wY zc5St`?aixU+X(`2D7Y#Tn?iq!TM% zy98cpLZa9_((7E3Uk}DLdeBnexw8H(>Z)kigor(=_)2~r+4L@XUtg-#c4dvTCsed? z>6o3DdgJKg7jua~=BO1doIsLyLS&{L_rM3zF0(>%T`P8AuzY8mBWwGZI1&M5)Gp=p z%iQRO6?!`wGpnk$09{0bW0g6f$KFEw<6WrIM$^BqBXQh1E=A{6dkL9%z7N9}5KCea z=>S6EcIM3grFjOHebxY7nAR{v{8BW?F&F2yh|_EIKpsVgf+!8vT#KOoT7?P!)+er} z&O??EascGhowm4%zmIvBZZPKPx@As{na!X~l5&KKaToTh$uA4A;W$L8OZ^13Tqx>c zi1!#PUS+M$e?PKjR}*I1(#Pl3-3(l?WdE%2(Q9V9*N%dT%FPJMeKHRfj`-}$NuNHl zj*woTG_$+SrwT;uW`U_eU&7{^dc2;_jp9gHwHleWD23`h_^y-;;Tn=oN;-CQ$u9lY z6y2{@YJ3yod%R21j6}$WjVN|K)Z`JIESr#&(&1F8#5~X+!`-~re8t-mqDt*>^p~OE z!aFaP2Zn)s)QlNxtb?gLzbSJzpXHE*6+B1p%TOi~9*Vb(+TQh`B0yj!5V3 zi}yC%KxgXOF_EddQ^a8}L>p$E2=$0=8`abVlLJ#EwL)}r3)Q;pFACrBpiv2T$`p?b zuMP8KmF(~Z){4K?U}Z?X3@~Z_7K`1?q3;<~6=GNgOi>gd3Sxky|0WJK*CCFg#z!sF zrY@ph2mq=vboWRtx+gG^zd`3q)yjmCV(mwl?@c$Rb@!{-8iTusSZZ+z!`C#{u*{4i zi|)?^ILMje*3FJ%@%iM!xnPB|6P{9BKW4ZB+PbdrLYJie;@=0XkwT`zau0fG8yxHK z@J(^UBf|}>iwWIj#Tikg@U8(fYekc1_}fqmF+XlI2A!Ko-h>RNmkjBquDwhuu+GH#xGKJI zB2D1;R||@w)*ZC{;SsV6As})}zbH=S>PV}&tyn7R?lAprm3qQ58J#Kq$D}J6PyED~ zpSDXWOl*ak-O8yG(Ua)28{-MO)&^#a*h7X)3nz-qxBleA6A&4#N&@MAXlN_9Ia88)N8bU)W>1AgSLTGbF3Q7XwZKPcV%q61rd8p07^h ztEx#{%Zs@aSTwqEX=DVE&6+5pZV$Q!{Uz?ihou}g6;WMWbbg}DK7kJ|%abv}oOR8a zkhQ*k*($Vtt4I|;zx+^M)l%DHK$2R)QmpeXoSK#xaBsTU4F^|Mzh;d0aC*O}$J`o> zC!}-%%gvya|5m19m~+JP_tZFN)G%NqMiqv5%4a(^vpwYGleR`EN^v4=l{ANmZ0wwz z8C7V3O=VEwGJ#o;NYn;p|4pFN!3krHr&_YqSK7F;-AxnDE~W~N0e0;uA-4Cy5^4A` z`h*Ol!Ty|*vP}gC%8^jY2aYkL=AXF&1m zuP(ky_%y3&gT^zxH-ErQG=OoS+je;my}{I}zV!zwd6+fhvXYc@U`Zo#kJ<`XJ9$jb zOxh`FlIM^Av~Aenvm2vMYV>kFXQ(ZQ^5F|^#b>dx9tC(3noM&N3b}uP9Gtbk8FyB~ zL`FGr>V<0ZL-xRy^j_G@aXm4bPLM7VjkM$A0+9&Nah4ZntZo( zdMgSnB^iL?<0IXY9N}*$rKAPG6B%5g)X2x8T9tCHEe4L(nwnt3TQ2tFYv~NP)i=d5 zAw(j{2-4!!H?pyu!ZH_~qS}Q(Yw0xQz!buY^U9E5*WioSY24dSSIRl*LcEjq5Mh9z z>Gf#7F0d>0C6l--lq2`PHwg-F;~wK4eZ=hjQIUFuY|m-#t7l?#tg+M5G-k7YRYj`p z6kR=@U!+r~QOsPDn!*uzdd35pZqlFbs(V1HstIX*gsav$4pVE`>!iUuiW^1$L;o4W zVZs^Kk^ZW;8LrzxpReS&NA)pw{LDW9wWDB4zB#zsH>saIBQlyD0J`rJu;V5EpWOpLw6&q?rzrdn8HSl+j z`4yX<=oL$}+BUQHrq1D8(-gx!NYZ$;=I9l|%`g{J9n&J+lH?L2@arCa13ya(A$udG zhm=n%Vz?z}3D3=zonxw}xPr9i6257!GWP_z;B5A5k2 zHiq=l(FpSvC_{c)7NuAoZjJKQ0_KTY?~TxW*0&DZX9DFThKPHo4z1b0oVN0E(}r@! z2XzTMUB=6|*(ni>P&izq=d^_IJN5}Zs^yLOt|Bd%&^+a}A*y5MP=h+vWEW1FnD1D>;WcW#692n?+XE@qZLbMCrh2hrCIx@+MW(vYZ zQ@sLe5J5Ltas@>FCr)mq%*609N`Ut0r+>Vwe z5RC*alII6INkz@Nu_>9tqJowUW={iYd7Mu3RWb3s(y)ZX5}BXT@CM4K;_Z3ZFx@|Q z{zGU+P}(06qwNcQJhVMz(c*}5jk06Lm#e!JEQ1+i&B3QriVXKmR^2y8aQvJfEmPw> zD@SGjl4yYaIeYH&w+xP1HYPd*zuI61g%|4L#d9*)P&lQ%FfKN>E98urt7$+Frfp?v z2ii~%Dzd!|7_F#A19`W43Jk_}g6{Vx%v90n;k0Tq5fbf3cTlCGmfyvoWa{o@AbV8+ zpbP=W6-E>y@;S_#1P$wT?-My)YQ&Lytz%9`aaQ2a&*p7n2YBz-yv*ZU7k(~^!{aQ* zeRc=hK*X7?1@@1=%hqU6jrKMyZ}!A8@EO(>!Qw+;cM%g=JPa>m=t~%PW`8~%+-H>V z76aq-b6eP|ObE#wRI5_Yc>$pk1y^XXxk|z289VI#W&UFpfLI@Em$WBLYtlujbZR{Y zHy9jo0_x#CH!`+x#JHshzk-@E3&ddb`{LF*;XY?n`Agbt^S5uSv#!25#n>qx z%Q|bry{(A091#7@%2CQZt-Dwr&Eb!ZOBjdFx29}&&kE@*dXID^O2CvreqfIYCfb9Q zO>UZAS4-wPnorC1`Z++V#|(-f<(zTVN(>JR45A^Hbxa6C;gC)O|t*DeesfPk|vE-LexX%CiO z_E1oy9oV}}EgsAWuK5x=(>h^L7?0kSa4jk8C5ClX?UHNJKNG7>ffv)Ybji$c(wY3H z+n&smDr8YzL%GiBBhk1{L@nyt-KV|?*U7@BQ=`JtY9vV*e~sp$EN6=8n^N=37C9h- zwcIw*PxS?w;mM3IY>v&idVST!*u4IS61;a+>gw4B`xCwnDp|@c|0c*BP zUB^M!rNK`yW(u2$l4`#$fEmC2CwvOoqJJN8$tRMj)h8q|Hdw@FVMh-d#l#+Z#H-$n zWMmm(ya3~5nP-tn1uCW{=L;7Xcu=w8B+z50R(E087KuE?xM3pvjG(gUJ~hDN1w*gR zTKQY*pD8o%_5K5_hJrnn6r!`4uK8-LRwG#KbvYbDD*T~XJk4mtDfsyAi}~YOa6ZAc zb&dHas~k*1hwFaji86rpII|d$(UL?Ej>_BMmqcnGWUNGBeb?Z!#cIG40dYj8EY zWF9JY6rgMsXsLy*&fxXb^0zqnvC3tQ1y4QN#fGbaio3FBY4YM^=D?3X0SECxxtU0>u0e2&WDv%(3GEB zz?6){!%*t$D3kO2g$acoBn*uykP@|#(X$C!U1mN2?;NKUU*Ocw;@I}Z1S3Ou0)Tt? z4|KSDg(ynb8vSx@CT+s0O%yv>FOg@(sRFAHOhBa<^C!IyITKyNUAaKQ$DFWHL?fFT zABWOYgz_p=MzeUb)PZII)Fn3JPDWB~rDNu5d3AT#W#QF6~h@amClo-LtRM-tTcOfat(Td9W<+k$pIQ{$k) z=+b8wiJ^E)*3FnfgA^EjBQ2>pm;B>hw7d*YXNCg@cvcZwT!?9#)43&DYbfgc=a*d0 zl=;s2S7*;5Hk1v-B+r+R9Ir9GbHnjLa=#s`2kAzsV(BQpeQ{_HkLm^X5ct2jZwqJj z^5IYIhE`d(Z|0j}KB=rs4I0+vXrmcg-Tf>LY&GIrhMdFu^(uXfy()HLPK{;>cDTdz zC4b+`&VALG9Z3bEA+dmc`D^^N^?;87im>24=qa<&jnK~5lu7BZWErz0k343X#bZXt zeWIE*BZ#U6*reUeykg0>5{e$yat$=l|C^U^4y4_*g6zS-`6Uh){Q^nxGW_~fE~P3{ z85e#`?M7UVrkwa4{MkPLo|RbK6hyTKe3jBcH^*u>;d-ph1a@8%r86qbtCiUoP7-`_ zlS+lY=L^W)(b2$Ou^J#)C~$`~qBv{;Yhc{OoRjOgKfO;xEnjheGcQ0wHAjzwS;5RG zGn#{s!CB4vREz&Nh3urp(2qIbgFVM|#9?$a#7;&o=O&HaMLmsYq{)3z(~k#C{ua$! zWcc?Swu8m1JJ7JOA*EBbq9_4U??>9m8`U+`A)PZlO{1-3p=t)K7mJTm@+SN&MMtu! z0X^S2LAt^gkIJM_9AJ*=*A>Z?oe+v3{^f1%2+ci#{3q=4vj?*OL@s8)4e9}{Epl8s zpXXR8Lf~jB=`COSMtig%9?~*=;g8q%;K6Q(I&4)#B^ZZ!q$utt8uBcm?WfZron+K)i#=YlgLb!q8xn98@DeNb=w$PG zC{w}7?F2E&q-BjcYuO&Ie>9kK67C0Ku4A6o>7iZeQZtnG`3U~2{5+72(*F(=uEYDD zMlkJNDe+lv8Rac#f7mgEoz*8-VoOc!r$Rw)(-T}DCMBQNB@nn&cdcwxr8U&V*VG_m zoW`0s$9u`W^;s0|?{z{lGOfLbK_-0YGDgonkl*TVvG;U-l6@%B@!{p6i%gZ3CssSV z{N+B*hu9i0nhkgV@)+IuTgBKkg%HV2E8=_2M7dn;z*9g-X#=>k2ZTI+0K@Dq%iKct z=FG;^4ae#Qbgv%2JQtlpz>mAdZ)MhxNvENmg4&-qu`Ka_kp%SUhS!(6rWIG<1@zJe z=1xVMHWF5MzDUSrIxUOJ&YG?~igGqQWs3E{9TLfUdIU-oaG_Plm(6idS^D9pY*=Hp z`ph$&JH5TUS)C(nTt3{*HU+luzaNffZS^2&ACY8_s>pc1%T-0E_yq@D`3fnLEvE!Su514!IGktk& zUXm77(f_cEnR>j-yiubJ`=k4D0BB%;51ClQFWsoT)n$zu#L8tRK|o&&BrptNl2;55Rx9wDL~E_a%)z6} z^vdT>>ohRoCv)jy>PA3&CAx`yl`MeIZNkAs3!a4s9h+?&ZY0HsZBB*RLj+IL4;k^)zA>(t1`xP=z z8!jzrUMB2Z|L6~O)^^rBo=X3}P$!=61s`DdUo)T2)Tp3;l~aFnB&XeJ*Q#{GoDI5W z;;WKS(%WWkl%QAXp#eAi>evWp_|D`HOA0^uz zrPejF(m~GjZU^mzX#(C%&(X7t?4H?!kZ^)NzJyd@i108JY0^1 z)Vw_GS(Kx5K!2EGQ`+Hk06QYTz!67;Zuy?c+Ka`08KJ=JX<9jbdgP;?9f+AQF66HYYa)`1iGC_<|pEITEo8X`f z#m3}JtC|yBE0?;F5r9~xpz#5{f$%pr7+=2)ZoM)%^Mg2ijNJ!hK+XH8vLmPc%k7Kd z({iJQe}FINh^wM*CU;gBXffKDt+~zmy=HO-7!|59VfsIaUdGmPBfRTvE;okCH?oc1 z-}ZPnqSX%yAINE#o&be)j&J!MCfM8z)OdG z518b9Vk*&_I=TClgbVrrla6QFY4wLVsy7@>lwO$fun~LgVgFW`=_YArS{#8Mr0cve zWa5)4!cG|DGorf+KX{fYHff&c8o#2O*3pYFeqyI_5H|_z(MXvSZ_i9^Jxo-jwdM6* zh^`~Vy>@VSfh;>Z6f=4C5`VMKdjI&f=g7>*9J2zz?K}$RX(*f0Dj4PuZ|neYaJG6E z_KC0O>nwd^UVpf-NKnB=kT3o?P)TeVO{|d~k9hltx(a`zNs*pEJ6;c-i#Ys` zGLPM_8Y>A)1T`pc1>SzX_1L|jekdva-nvK`9_BWjvXo`PK^r&idR$G?S#;WGO9#RR zny*xTgO{lDVK79DLp?S{>{2$P8UFw^5X{Ev%v!!-+JcBS)0p{V{8*!l?~omo+Q=Y_ zx?&o_e*oxfKUG|AQSBgeRm=v3_4{b5DeE{j*12(Irfx^YY@3`p!g?(2G0q22c{?tf z$vcTuC!LIwzjMH)JZ-B$N`2y2?f(H>V*|YHd1irin2|_6Vau8C7U3Z8 z7p1tmzTj6zp^QpfpQYEJ%L`?E9mA}^4QWqO2`X(4qk(OayGWEEbPFzI+d7&ypV4dV zW`%mbzwaB}Ou#y0&mZoCG^3r>^R8F5$9*5FQlkmIohg?dzd4YDdu+RRwkf$@cW>g? zp67rJ$FlT#_uI?0r1nyURF621Pz)i8_Bb^Gu@PW?5WokGti!(+95 z&rBCOR@*N+>Q8hwBALTGq$eK>`H9Rt4VRU>De4R~0eP`mq1pG%=z&LxB*c`c=uK8 zYab@tjgOUL+kl~zL+#x`@Y@`(37Lcg0|i#yiSm*BvEG^hLnu$)$EiZiUweOOyia&m zpGIy&YA2mMMuK;d1u46qj~s=h<*bXQMc~j%=(>Ef^Wo#yIi;)7TXEnNTOTfW9c?Y9 zL>B#Twk3QsJJ52#0kAC3V{Qm@VXiv-OAmF4vYq_w<_2PFY(gkgBR`pi(EpIn=1h72 z7I{jaTv^|u+wyhCBybyI0kCybV)j^s6)7JvM#`ks(Mp#T%_vO-xLG}1#yW5Vze%+| z+~U?k9jNH#W%8T3>h74CuAF6^go%!OAYCJ3KaLk<$b@O-23OvWX;N~ zeNK||?3^u6G9N1+I{=`Rn4}m03=9AO`}_bt)&ar*co-O17-)D{SXcyjcm!l@ATkmX zG64oADmEDr1vx1ZDG4PVCle(N8x095NRXNBD>ok>9|fa`q%e;JCodoOKb?TVBOo9n zA>#vq_}tW_)ZG8y<)a^f3JWFxCIA6O3IIn1gFprQ7zE$}0N@Z{|4jFP3K%#9Bos8v zrx5#7@c+sH{|GKe;o?{YC zBnHvPmYd&)MAc4Due|I_4o&V<(|*=ewUt-N-h7-gjvYE3FV`;|(>Ujq#*l1)!OwXi z3V$}Bd2^@2DrhssXlvZsuTh}RsiQDefv4>qI z!6R-0kH7TrpVKe5qL%pVyG5$2 zz3Ust>Y3x!Bq}G@=pk=VTC7hOw&_lAICg#K)_M`(e)j*T7qCDBaDy~PCTlFdES{IL z!*vz<9!{uCx)wG!HS^6(GuP?hoBD8Tc4pnOVIS|)w$asnFKxDRI z@!X1%som4;#?|Ppxj7BosgfGX!2-tf;YbI|ihNO;>R&p`C(m7SodVyS|G5JI4Q@aQ zYd{Iyc|p`zrWw3_shRw?v=%twWm{Q}3;wJ`F;}iR%XP8Mu!J&uEJK3`WJUt?Ap(SD z0lmWLCgR!bMz=4v<;qjf&9<&N<0YC_n`jW**koKH?LIljdYoYEk$_M23IKY6pKI{5 zvO(#YHywCBoVV*Eua(Fa?sA$?#tFn&1zT$#d&v`=@Z$RlrmD3yLI@y12z;s*!1g&w zU=Bp;@VNu)dKq5*^=XAZ>ELh8v7}`wE)qOv)8NTvWv44%POO&g?9FM`<<=802LSWG z5BK>6HuVi(D(RGw$UdHZb~0bjZsW-+IqT%!pmWs)*SP%@3ip~}EsGc5)T(%@dWt^% zGRt=v*Z=dz&qWQJPysFEqOEgEyR+oF3Dq}uc=xth8ht1FU|PpetU$W!h%1X%pR~Un z)nHjpUu-p*UY%Ek>xk?->hM4%P(2<8pzXIkHlO~c&n~REERl3pp|T`_Ry$OU|Cpbq zKjI?C`X$G4HCM@WbM)kK=v3Z=E@`Q?ICa^CGQ***cDmb|@X{|cW|2(`14{fqwwbHCbvMEajFKU}2 zj;W~5^`iS&!>X{)HSSc1{7h%S|$G%R0}SVn&`l z@{zf-s>#U=DyGNmU{g{WWmhB5XAQk{l1{zv58m$u8|Q%grI<|YeQzdF=E%Y6Lt@Ja zlD*ezE@@uSYqzqkt8NBMqg-tYSaWV>FoS1>!cm(`TT_dch82wPK6SM%@nXcHYP!yr zXL%F7ms&fj5rfoanaVboX($9|%z~vGTxW(4H>EXQ^Bju|{8FvLnc{;zq+_g~)p*uHD`KwmAd z2PQy244T7xdgc#u$5vDNmk)p+!QrwuKjvJ$m3VP7r#8caEkn75YczEGs9aTjY<#-$ zj&?cecj6iAH6&vxSMJ@6!Bsb^kdv)(-rR8N`V`p`p&aw`;)EGXm8pyi= zu^J#tm7&>L)?1V@ysG0^<$b5RU*KORg>v`85j zgF?2DH|K*8d)?xY4ee19YfM4bcQ_BWI!*LV&Y9? zBo&7|XrP2a#+lb%6hF_+j6GAI^Gefz1{6TY9<}Pj+K;g-5XbC9W~-6Ylii>;D@@sL z5y9U!97tO`L~f{BK{w)dXLIJfwvyp`Qgr@OU$y}Z?`S21BQvk73-Iv6?BdzM9Kzy! z+#W{*#E9N76fkCHKw4-(dg|siO@sAov}jsVimrxbT1gClUS_L@d&Yr%O4*~g8@Jlh zxR%gUh0LI3PCq8N9W+kbA6Ce#t+k)r>V}FJO-Sp1&u^_(%){CdN{L4ca9qZqYCld_I$-`rzN1+- zfS>mINTy-@cikr-WO-_w9zR>*E7Bm_H0sF_B4hf<5T8Mho)bz&g)vd2zU{Z%C~47O zykh8F{Hs~y?%sIqj4b!L$?Lb%Pj+3i^|6VvV=e6Tf}t-suT(7y%I7PuneX=(?u)Qb zZsw*PEhJBbcE-NT(+u2W)5*3bEkP?yk?aIP_#Mwuo(ssic9c7=8bjF68Q${=buf%p zik|nYX&&Z5JV>Zw3=3LwRZ`z*o#GI+?DzsG!Z^%>73qct{W=5dY&%d;JGbt=W^`kq-%N|*QF~)vVgmI`!9Exi*J{e z*}!S&#(>^B30N_!^EZ+KLV4?259_EJYQ?>G%?ETprV+*0qJ#Xa*WBb|RKNL7PpM%A zt&PLGsM&Iq87r|w6Ti(3l-On)1K9=)k`tExT<9m4i5|9@8b4wVoxDZvQ6uXx$u}G| z8W5LhoZ{h+uV$Z0P0EGqwy7>L__Qcg@l;|0A=Ok>T(^a41bR{}pJhmvlq2pUgjsK) zBPU$hjf<7UQfYYv5eR6gs-16mpYQ+HHe+W#L)_Y^o_xPsB1$?JZ8kJrwPMMZs#n_> zQZ(=mw$>;?nYz7~ie7NnY_V*ji1?%GC1-7~yE!4?Ny}$8#LKn4xZG2yYuP4*)|lx=!Pu*bbYA)~cct+b{e`NT?oSmrjN$%?RPqtvsa zC(q_P@gbJ%dRd;IQte(~N0ChDG7)qU9jspLoEloDJza6Xt4ir9)^~Mt?t5VH^ju_7rNi-3aR zzb^GN5#a}WALI8gi}#qG7gItkxc;M%=e^W|MZxzjJ0=0l7N+58cQgZl)a6>Fi@- zrF(^R8A9C51aiL`2*RI@YOZpJauMo(oevo@{hFuQ_=JY!6B=+Z$j`_A4=(>e1C9y; zejyfsAYm5#Pn-e%FDS`(^nSvgtqsoTx3aIth*2q#Q7kN#daQOGFz_TXPWcyDLd}wv zUqV*8Gqd+H8pWlVL?+inB3fd~u5?hLqVrBg(beL{DYW#7aL38jaj~kCu@>~5dpZqs z6nDDfRcEY*Dg_8K00sd81_ccT4h0GEui8G-Mu;yY%#frkf{H*i zVgW-(GLTR}JgbtCQ(oOHxp3dEf^x!t1xL{QpTW_DeJ51aIQj>me2Dpx?k@9Lzjz3P z=4cm=>bo4*){M(H*UJ(1d3fqj#g#A3E;U z&Qy8C2Z=uA2^zg}?Lc7ZmY?PE!~UR}BD{J#-71R9%lGfBo1j0rCr!DCHKLcXD!$$* zEhfeM^VGZ{f;UG6&r~XI)x2#85cNCxmNh3Ie?tkCQn%}QVtgZStq{q1YW(m@i{2WExlpb$Fs;j)nnwPSwDa!kQT^UqiX2)H4^S{hYzDXpYB zX;BeQR90ceXz&rJf7#?sm!)Pr@DxT?&XeP9PPDHPMQ$%JF88ZMT@?HPoOlqMFOH>e zd3*NBhNQ$KR9ROOsCmQW2?1|{iJ-GHJX{pS&0wqp0r`Ju+(m@!gXGix*fT@?0*3C2 zCwHW>6JiszozoH06|9d7FXjerMXQEA5K++7U(8abY_d zMp4F_&g?>L72ZB;FS5*;_)F8qs}zP$YPjy; z1q#Lb#yI?oN7^SW3TXX?Dz}li&r6a@cL~;m!U#52f9}d|^^scU|FE@B3 z;UCU6u)qCaaB%nrInSg2O;S5x&@zhb#rQdwHq$KgZzfzp;%Of>p+Z(a96Qb5Udu_~$Tx`O4R?OX{0{ z#_m093jPe>cPbv?xnTmeTKco4N|p8x2Tq)pubzGF39NZ3?U5I)VVAuHzWtpA2}?d3 zBXe&gmT+ItrtPDY(WdX=O{qu_N1c^&Y3zI1@J&Zl*yj*{=XLJpSX+*}oRkV$c0_oV z$8-eA6t~7O5wV`v62MK_q9NU@#JUj2R45(8r#F4G3uQ){8`r650Y574h&N!##vwNF zMqiJy;)Gb#xDX$m6Hr-C`8qXI(S*hJT~$5_23Y!KJ|&(^R9ak+jP^&v-zf=^Z?w0n zcNTRv9{}`sc5M|dZV3d_Otsnc5z&Tb368PqzoQ*c9N9lY;T|rwK+&m01{vd4Fx+3A zOa-a@2;e)MJ2$~p2#`1FEJ}L8I^cuW)~juF%B2gu6EZ#0%T%7z8Txy3uP47n%30z- z-U&`F?wKFx?FWp0gNk-nB^M~(vh>eJa9&i#HSC99-L(FJ-A|<2q{>J+r1FdvpkPq} zn0~mYGvBr^UlT0ezG>DODNye9wD)*b+{UZYYPvbt90;-EW`0-J<)Wk#ZJcLvj0QEE zuN_ZecyU>f3Kc1b#al~Kqth`p1=hz5h-bSY;ST_E%Xp1j%dzo+Wh?=fTG4X7UnWKbCMl#;$}l@Ig|3skkeqn>LT(Son+-s z2K!YNih1+={GI2NlfwBgM3dT{Ptmhh&xQB0dpU%gQZ+Jb>oUz4ZcT}x!v;QHu9O-2 z1y@MGC$gJPC%)yP!RWBv96<+aw<7|X&bDhe#E0V=}yuUzJa~8J?{@(F_9@YX{ zYs08>zS3TzAL+uUv3&sKl6Gy5h&c1dIdQtq(li{-5|ifm!@DV>i?H5}x*2 zVg#LZ^{$+gfv$LhgZ0jBg-oSH z@)>J>z`6sp4HJ8#gQf z*rbsArgerY!xfBs{wEp4hu^G3^QFRnI2^7)0USS*ohv=RgOSaOinD!4?5z9)P%;e% z7t5A6F|w+X|2VX2Wq0cC-Tuer1^rRjBS*m&L{s}NRqZ{WSiTy3Icw`(eXqias8^0Z z8Ijz?*QDX}S#e}^?s->>-jmNbg_9vymZdo>R#f~)=1p!p>5!PE*v*=2rc>!3 zE`()5+gZ*%{cBZ=V(8~!jjnd(gJumwZOO~-nz2>kG{JBLO9vm0fJ4HiBpg=%;UR~cT@fB$I`hO@Tev%Ks17wdeTg4nEs#F&i=?6eb zGgq}xqt>nUFX{=u_t-Dhuh}nS=I@r5zg%(kC&D^?n7{djP`I0iF>;QAPchlbId;!O zhPJs}tUiooW6;;xBPR;Jh%NJDL}nL+{s2yH({npylEQix5n;8}I&doW+7O*t*7|i% z?m-(hZxe{M1!&5+t_mc{7EP^g~S?zZ)axUSWX>lguHShKe8vM-b)U4dz3@8}JVGNC8{q2*$=76OeV#d2JS zFZW5ZBOe6pGe$}k`apX9!l(F_e8wH#f5gC*#>nW&2o&Uu)fsxle1`~zSz-*&+=^mYx%?VRA|ny>0oe2v%oQKt6)A4T z4!wjaQ~$x39}OGtGd@Y&coDWPV{ORub-sWUx41q%)D>oWDpx6}eEUbg#v?22VpV^d z)J6-@9KR`F5pf8-|G?(Bx3TwfAn~6|gbqUk{XNhPMeAc&Gd95#d+zo^R~#}|6fLwJ zo=In^?+!NoMUBK0dni}WB4ZTEypJeS8wEM-0w~Y#5b_cnt?OFyjwi1&Ke5s@!V2ao-xqL0QfCdn2*pq610J_JBuA|D7G5N zF#St6lZ21|pmrC9zVO+rT3mN@!d~S5u3(#?AE6a!p;RfXWqV@HCjCN~NCUQUXqQ8b zenRqfsW*?l5bKUwna4Y=RCBujXK9Y45>tq(9sGi?qt_eskq8qv9czGz-J*a>xhqy~ z-zxf^=99kBKx3oZ-nOLBZScy;9w^p}2g~8+AL!At zY6Z5#gQeA9VwscEBhXZ+gN^k~Wc%gFH_gMH+2tsedN^{CNm~cKMT}P}`%=UR{&@cf3k{q}T>hOTNd8_1 zTb6;9%K^`ylnft@rn2^;7{_2~q6U9r42y>|%>1_z2P-I8q$Py)CF9vTnT&y*mKLV< z_Rzz7-}rTad!n98Dj;+!OuI9Ue}d!P(9F}OV#rrJo8;UY94x<_-~N{AC$}nnMY>J$ z6(8%(6Co;}-6qEwdw=<`*1pY7Z>bp%`q}|f+8N2h-ion7f%)7h)zlY46N8$C5{@Xp z(#rB0{>QnV`IUFWaEzBJUa0 zNP9x35pm>*;=E#{UeO`5Ho6d%yFr_0J}G=L84FZ;+k<~_bQ-K6WRoR}3=Oe#V%b?s zIlLR*UfVr`vibmAWv_`SzkU~ll6r(j87wr!liD+C7FoM>(6dhF%raafjiotp5<8Y9 zbol@{N*zgRLNjCYLm)aMiC&!y-=4#IWY?+tM{+lwDYHdi%1V^kKc(fg-_kZU^p!<3 zUTqHka=~MzcxO5hsef!r`4-?MD{Lb5qr2xoOyoqTcdBXdi%6}?d)~AFLxP?*@r@jq z+cm=u3gcncP>X}RpMi`J@t9|`G{h2DsD@sGBVw^iO+eaGdhabT*pAH5g$;f;g1M#K zGGGEW96`orSD8^(d)PkFMrY$-qa^MyxEVLlk%gEDC*{y&a2Z%jUP({67|7Ob6XG)< zp`tr!imgs5TDZFC>F z)*-aRW=6U7-qJDN$p^?e3A}7>M6Z0 zZA+OL*&2|~=0r<{gb*9MjK#umwz?ezf?+#V7yZJNVkKo(8Pk$Q zfi{x2JasU^eqsG;^1GQ*FWL9gHs}iuZu(b;gfZG}8ZBolg0lSez6$8rX^bScYv^o9 zHm<|rSbsHZ1sU152c+m*7KW*On&!iGM-_EQg$Hv>Q$)>O>hiO%vIij>dxpKK6VyHN zMyWFhkQ?|<4|6B^4ap;~;aMRu>6P1k!71P73M;b-;{b|;VMVkj_A)B+b%uop3 z+m$Xf+UE}n<##JCSbjoUDiXWO)yE9hHfV-tz^UGp=D>yjh#N-wZgk#&bx8yeTtFEum`wc-7Yt84!`4zuopx%w7gGz3#5Grlc|b%avLfbsn_K-fOTKWR6zq?G<^ zML0~UJgxk?LeYqgjGoq8R&0+f5}0BBnNDa0Sc5 z+h}r<+*6;~awsx@f;)JY^`ES(hfv)xmcVw}fq!z8Gt|)NVqI`&cAM}$JWT_~sD?he z@Z=6^tCNWpRc(!pm~QsEHNvntm#;46=N}LPApa8Ov`E!JJG}+MrJ3 zX`iv?11dGZ(&6H$ZY`&c%pw<2h_MmT&xfr_O2!ywGB+ISz}BT>|N9sR z;2&}i$U07cHNb;*+4^Oi{(1m^01-4V3wb>S8#Dp>Y~N1v+{WU^c~}+j^L(}kkU+{M z>>cUDWrU#lD9}CSeaipZ(xKkM`ko2%h;o?EZfTIywBInJ$wD-uWr*g9Hg)Lq$7zJ8;gfio1z`4v;^y34eGW9u6I_DK zE3kG;L8LCvGk}8QdqpaPu6ql&sO(m|uba)}tz?8_EKkThfqR4Gn`iu{zr3TM5Glvs zPBT(^-f!fQyQh&z{&O(qle`57gZdA9`)^>c z2m}clItIiOD}09bSN}t51K;-$>_)aNzE1Q?uwXYxqHXYqGO4}f{NV6TmHfSMPq=&3 zcgGQsLmtl~Zsx5+Rq*R*S_x)P5;2=J`qw1I!liNL^o~@Nv_7XCkV)SGSA5akCxPIl*0pOb@lI%?cdQxE}T@%zGE#<-unGCG>;+4 zem)}#W6|<_YC~kab|J>s#p8rl9hq<)ylDn}QF#eSJct^h!t5Km2JimX#1m$K`L*}F z6(qZ1Kp-Tm*Rz9r$g)ke3fUhmB%-)Dg9D`zsE>B0-x9?2OAE9u=S51-4x z=IU;hom&s!K$QNZZc6gyP)?kxTdyV~U`Bw{Ey(6&Ps*tEG-4-7fkA1bdL49u$5?5D z)d<1M2#`C0I*6^S>qd#>XvRiZBwyr~>+=eK2@*de%)0U8?9M^uQ@d2^#Mfm<69~@iFgf%I8HBV*W5%!V2?a&V{P;u+b^@86=wX zrJy|6MNA+&E#%Y23iBoknPgpHwVAov<6#JQATRrhIr%AKe(r0K`QIG%KTG-Ft%(20 zQ9mpl>6gj0w7wW^* z2{FnwY|F`a7gYN1%n{)nsE;r()gOSB@!a%^W=|tgy-hML&i=m>eUFg8M+wG&&TG@o z;YHm1gB!oMOiw=(kfaV4FILVVTeH_ygr7M4e^L08&h#t@51sDBLQZ;-V-5v`EcS8H zYv|~@qysD$uq|>j9@EB}s}9l%ZgT2NZF6GDA2Yp)xwH`QIor}QgvF-|nb&i*^>Fm? zLha;71(N&FPRpaHEf4MX5qJnthlM~~K^n$OX^0hj9l?c|2*aIImSn!!vcAHs zLb;`@5pNQ(7OG+mWrbC8dzfAK6NV{mM2}&j5%5OUBX@qX{*k!bx6%wp%cda&6V!*3 z*pHOrNf90_H1pIh@?5<~#$P7;AN);Ex;&yM<|<#!pHj&!UWHlR5W0zVbw=89Hx!I$ za|}SX!c&i!9KDAVCUS?|p0$_-(pEXqT6KW|{lbb~ER_#GhKTfc`;D|7Gownigx zsfRhexVwWmsEQ5d+R9g1xUj9m%IVxZ_tbD&dBhY13=u^!@8K%|RbnJy1*NF4QnH?R zX(4eacxe5~AAoB9?lYn*;UDW4W!cIT^}5~C!Sp4wNSrs0;3Gp2;1(xjR>q_3%0nbO zCDmlE3fF_xC*o;7vZ?h&iasf8{V?s+$kl+G z5wV6gojUMwN0@}IZ8T;%t+aA*72w2_3S$wIA{IU~ivSkX26&Uh7X5uIlp?fl?!k8l zHOtvT`{g%L3DLa<{csE0w+E;gZbmGHV(7j(aD{WPWJodYX!h2LbaAD+W?OX#)}qL= zjGx;IMyv`Vl1dv5rxWl{Y|7?;!;ma3-IMh>pTg(``%a`d1w@|$?~lO25|onvGPsBo zu;d(>A!`r}stzrTI3A|QYkQR%?6|y$;)kBaX-aXfr~bZ%f9CMidMCyNo2WfXv(ZuM zjUzzSK?s7=vJRn;=MJmJGxO;UI;Qp`svQ)!zhw1dU<7B5v}OrZ z7l9`##h^F^4gYq$RJf0!mi%GbOVAJEcUB<0M#-uRF;GSr0oM*mN_t=iY*aE;=uhy& z1;<5-(X3wC@i@~?xFA5Esos%UXCVo2B8+bIu4*9)ICI+}k{`4hWz}}oF!xy^kTRG{ z8^#fGlPXp4e2V}wdZr3;;cZNGd!&iVkQpX0W|3#KWpLl33^drN-UK*>66E_^L)P6z zP&z;~m%d~uf7ObiR#?+$PoF#kkpeAzI|ZEW(>*Z5i0FJ&kYn)6b6ElJM2*d8{Vm{| z!ohK%V#M)~dgVv#!MUqT7PHLJm_?KU5$Tf5g{-;o5mp-vrWdcEU`b&mxiW3Z;H3!*__Jc=Qi$$TnYWASq#%Q^etqKu`joB5q%T*o{br5f5Hlb zM^a}j?PB47J**-9j4XCv5BHHD& zzX%omNQw-K8A8bsH)!-QKLCYRht!(EPiTONYXHz>8OJ|xW^rJ$5E+Igt51x=oX%9` z)K?hI0`dn8*i$kDrlTW@GbET&doYfd3{|*fd$keXX1E3BNwQ>Vb{8pga-0Vzr<%C# zBn(8DPEVPgomnxaD?RKOzYRZnd~IPW`26`~x{k6LWk*i0d|IqyW2crVY$*0HJX0xj z=4olD`z+QiI#!D6lEpUz?_H%8lHlgMe9;4;|a+a{XivTTV_Yw zn#!1nifjUKEknXhNr4;eLpJP*mkM*RZY{ZOX*RMeq!_|ejN&$N3DxsKv$uN|AAo`c zq<8Q=xPsr@qjscf0$@HF-%>abyJ1vM)18>)5k3GYWlG0PR#YghA8)(**uNutFGEGwrj(>OXudJJ8%z9{^G)m}U-DQEjFG_1{n#v}l9!5;S(U zF%uWaqa>u1Nnlw&H1KVcHQ*yGcZ&2Js!= zf&PIgE>ziz`7eOtU;M9Hkr_WmaN)~NE4CQbs&2l@#$vaoSW`IshCcjdPE1wdAP`ZF z;6nAcEL{aUfXmJ-Uu2yK&IK!#;uw}<4i?+ z1P!^fm3177I}@%7@`((pWWFPIp>_E3lFe_0S+KlDcup0AUS;lY{sEM+c>o?vCKyKD zrJ|p@GXpb>O4!_CS6{;FUe_1A3W?T(DB~qnp|z?l7^=bsqZboZ%JYtE~Um8lU3~P{n3M|?9!Wa#K9Ai4~;GF?Xx z1;{D^Z!6^$0=nwp3$IyQ@CNF?(0@=;}$Y6%;Uy{~|9Yd!`x`O3)Cj-y6P93Q}h{szs!&oW~@ex~;F-H5DteP(1*RG>k9$;iEuo`$`6n0lJAklCr?9Z^lww^1|gkDfI8M~xit73=!^I_eoWduXXqK}sKS})f^6Z6hOxhLG!L9jTT z&y;saIMHQ*w%nR$H^oozN0-%e7}`|e;syP4K09A7aDAxL;B1$vk|tUGZ33=FQE~pB zh0$~!_a#;=)v&T=a-tqeqGEet*eGB=q+CmRJ4Cl@j6>R=$*(S1V8}vIt=B*-?zE)5 z4yOd7cJY(H{2?x^XKA|k5R?5vNtP$lloMK59C&k~nd)(-2^Uczm9?yJ>b{@^*aTNW zSZOb-VPD6|)+qYXW2{$4s#(4Kmts-xIw_QS(uZ3cBo6i)F@haD4nc*>sNMpX+HSVO zS@DPeBn@j=I>`vL>0Xc6cjZd>u{H1u?6|RI7$lKqOixfe@0Y2ZNm>7Kw75h5Bol=1=+qBTNPk zn2~uV)k?gnzpP(8RZjNrp=%3tg7^p9vuO`2-vqgVr#e3UgHz+4cFvWIOHJ7BdNwOT zr{P)&VH)N|$ZRkGZyD}5hb)^ES9#jD=Tw?AWduk1U^$eK_dIR5*+l-*rxw_uXqNUX zvR!1ap0bInDqE@uLoi9j(Q)1DM}R0gt%WZ8+mMp7zqU_K7*OstG2=BgZbaCF8S#J# zIy0;dr-P@7ZcwbK$vYzK?4>-wEWz0&87F$9{fJow@h;`}M4zN0qkU|UXds5EiP<-$4)$BqyL5*_6b}TxmKp|96C@{#W3N1(;6Fn>;$cYFgNR?L0vbb8+z=u6`?iWzOIa=K#UU z!Lo44cpU~KC9B1(55RbKK|y@0 zfeO_c!!|`>mkYs76$;(wf2N!xJ9KJSsb+AZkAUUUqtS5gfu z#@@thOL;n%8O_aR!ln-{VFC<&P0cz<+SNg=fk62Cg{q5iY|RvN#WRNk`LJE?iOyd? aZ@c#*w7?dR;G0|Zje#i<@XlEutN#a=c#%Q? diff --git a/packages/shared-data/index.ts b/packages/shared-data/index.ts index 976896e2fe9cc..9f334c453e849 100644 --- a/packages/shared-data/index.ts +++ b/packages/shared-data/index.ts @@ -3,23 +3,24 @@ import extensions from './extensions.json' import logConstants from './logConstants' import { plans, PricingInformation } from './plans' import { pricing } from './pricing' -import { products, PRODUCT_MODULES } from './products' +import { PRODUCT_MODULES, products } from './products' import questions from './questions' import type { AWS_REGIONS_KEYS, CloudProvider, Region } from './regions' import { AWS_REGIONS, FLY_REGIONS } from './regions' -import tweets from './tweets' +import tweets, { topTweets } from './tweets' -export type { AWS_REGIONS_KEYS, CloudProvider, PricingInformation, Region } export { AWS_REGIONS, - FLY_REGIONS, config, extensions, + FLY_REGIONS, logConstants, plans, pricing, - products, PRODUCT_MODULES, + products, questions, + topTweets, tweets, } +export type { AWS_REGIONS_KEYS, CloudProvider, PricingInformation, Region } diff --git a/packages/shared-data/tweets.ts b/packages/shared-data/tweets.ts index cba89c1a39bf0..92120a4123b09 100644 --- a/packages/shared-data/tweets.ts +++ b/packages/shared-data/tweets.ts @@ -60,7 +60,7 @@ const tweets = [ img_url: '/images/twitter-profiles/ukFtCkww_400x400.jpg', }, { - text: 'Lately been using Supabase over AWS/ GCP for products to save on costs and rapid builds(Vibe Code) that do not need all the Infra and the hefty costs that come with AWS/ GCP out the door. Great solution overall. Love the new Feature stack thats implemented', + text: 'Lately been using Supabase over AWS/ GCP for products to save on costs and rapid builds(Vibe Code) that do not need all the Infra and the hefty costs that come with AWS/ GCP out the door. Great solution overall.', url: 'https://x.com/xthemadgeniusx/status/1960049950110384250', handle: 'xthemadgeniusx', img_url: '/images/twitter-profiles/XE8Oyngj_400x400.jpg', @@ -78,7 +78,7 @@ const tweets = [ img_url: '/images/twitter-profiles/GtrVV2dD_400x400.jpg', }, { - text: '@supabase is just 🤯 Now I see why a lot of people love using it as a backend for their applications. I am really impressed with how easy it is to set up an Auth and then just code it together for the frontend. @IngoKpp now I see your joy with Supabase #coding #fullstackwebdev', + text: '@supabase is just 🤯 Now I see why a lot of people love using it as a backend for their applications. I am really impressed with how easy it is to set up an Auth and then just code it together for the frontend.', url: 'https://twitter.com/IxoyeDesign/status/1497473731777728512', handle: 'IxoyeDesign', img_url: '/images/twitter-profiles/C8opIL-g_400x400.jpg', @@ -138,7 +138,7 @@ const tweets = [ img_url: '/images/twitter-profiles/rWX8Jzp5_400x400.jpg', }, { - text: 'There are a lot of indie hackers building in public, but it’s rare to see a startup shipping as consistently and transparently as Supabase. Their upcoming March releases look to be 🔥 Def worth a follow! also opened my eyes as to how to value add in open source.', + text: 'There are a lot of indie hackers building in public, but it’s rare to see a startup shipping as consistently and transparently as Supabase. Their upcoming March releases look to be 🔥 Def worth a follow!', url: 'https://twitter.com/swyx/status/1366685025047994373', handle: 'swyx', img_url: '/images/twitter-profiles/qhvO9V6x_400x400.jpg', @@ -162,7 +162,7 @@ const tweets = [ img_url: '/images/twitter-profiles/7NITI8Z3_400x400.jpg', }, { - text: 'This community is STRONG and will continue to be the reason why developers flock to @supabase over an alternative. Keep up the good work! ⚡️', + text: 'This community is STRONG and will continue to be the reason why developers flock to @supabase over an alternative.', url: 'https://twitter.com/_wilhelm__/status/1524074865107488769', handle: '_wilhelm__', img_url: '/images/twitter-profiles/CvqDy6YF_400x400.jpg', @@ -174,7 +174,7 @@ const tweets = [ img_url: '/images/twitter-profiles/bJlKtSxz_400x400.jpg', }, { - text: '@supabase Putting a ton of well-explained example API queries in a self-building documentation is just a classy move all around. I also love having GraphQL-style nested queries with traditional SQL filtering. This is pure DX delight. A+++. #backend', + text: '@supabase Putting a ton of well-explained example API queries in a self-building documentation is just a classy move all around. I also love having GraphQL-style nested queries with traditional SQL filtering. This is pure DX delight. A+++.', url: 'https://twitter.com/CodiferousCoder/status/1522233113207836675', handle: 'CodiferousCoder', img_url: '/images/twitter-profiles/t37cVLwy_400x400.jpg', @@ -191,12 +191,6 @@ const tweets = [ handle: 'JP__Gallegos', img_url: '/images/twitter-profiles/1PH2mt6v_400x400.jpg', }, - { - text: 'Check out this amazing product @supabase. A must give try #newidea #opportunity', - url: 'https://twitter.com/digitaldaswani/status/1364447219642814464', - handle: 'digitaldaswani', - img_url: '/images/twitter-profiles/w8HLdlC7_400x400.jpg', - }, { text: "I gave @supabase a try this weekend and I was able to create a quick dashboard to visualize the data from the PostgreSQL instance. It's super easy to use Supabase's API or the direct DB connection. Check out the tutorial 📖", url: 'https://twitter.com/razvanilin/status/1363770020581412867', @@ -215,12 +209,6 @@ const tweets = [ handle: 'razvanilin', img_url: '/images/twitter-profiles/AiaH9vJ2_400x400.jpg', }, - { - text: "Wait. Is it so easy to write queries for @supabase ? It's like simple SQL stuff!", - url: 'https://twitter.com/T0ny_Boy/status/1362911838908911617', - handle: 'T0ny_Boy', - img_url: '/images/twitter-profiles/UCBhUBZl_400x400.jpg', - }, { text: 'Jeez, and @supabase have native support for magic link login?! I was going to use http://magic.link for this But if I can get my whole DB + auth + magic link support in one... Awesome', url: 'https://twitter.com/louisbarclay/status/1362016666868154371', @@ -256,6 +244,7 @@ const tweets = [ url: 'https://twitter.com/nerdburn/status/1356857261495214085', handle: 'nerdburn', img_url: '/images/twitter-profiles/66VSV9Mm_400x400.png', + weight: 10, }, { text: 'Now things are starting to get interesting! Firebase has long been the obvious choice for many #flutter devs for the ease of use. But their databases are NoSQL, which has its downsides... Seems like @supabase is working on something interesting here!', @@ -287,6 +276,76 @@ const tweets = [ handle: '0xBanana', img_url: '/images/twitter-profiles/pgHIGqZ0_400x400.jpg', }, + { + text: `Very impressed by @supabase's growth. For new startups, they seem to have gone from "promising" to "standard" in remarkably short order.`, + url: 'https://x.com/patrickc/status/1979157875600617913', + handle: 'patrickc', + img_url: '/images/twitter-profiles/_iAaSUQf_400x400.jpg', + weight: 10, + }, + { + text: `Okay, I finally tried Supabase today and wow... why did I wait so long? 😅 Went from 'how do I even start' to having auth + database + real-time updates working in like 20 minutes. Sometimes the hype is actually justified! #Supabase`, + url: 'https://x.com/Aliahsan_sfv/status/1967167095894098210', + handle: 'Aliahsan_sfv', + img_url: '/images/twitter-profiles/2SQwtv8c_400x400.jpg', + weight: 9, + }, + { + text: `Supabase is the best product experience I've had in years.\nNot just tech - taste.\nFrom docs to latency to the URL structure that makes you think "oh, that's obvious"\nFeels like every other platform should study how they built it\n@supabase I love you`, + url: 'https://x.com/yatsiv_yuriy/status/1979182362480071162', + handle: 'yatsiv_yuriy', + img_url: '/images/twitter-profiles/Y1swF6ef_400x400.jpg', + weight: 9, + }, + { + text: "@supabase shout out, their MCP is awesome. It's helping me create better row securities and telling me best practises for setting up a supabase app", + url: 'https://x.com/adeelibr/status/1981356783818985774', + handle: 'adeelibr', + img_url: '/images/twitter-profiles/k0aPYRHF_400x400.jpg', + weight: 6, + }, ] +export const getWeightedTweets = (count: number): typeof tweets => { + const fallbackWeight = 1 + const availableTweets = [...tweets] + const selectedTweets: typeof tweets = [] + let remainingWeight = availableTweets.reduce( + (sum, tweet) => sum + (tweet.weight ?? fallbackWeight), + 0 + ) + + for (let i = 0; i < count && availableTweets.length > 0; i++) { + // Generate random number between 0 and remainingWeight + const random = Math.random() * remainingWeight + + // Find the selected tweet based on cumulative weights + let accumulatedWeight = 0 + let selectedIndex = -1 + + for (let j = 0; j < availableTweets.length; j++) { + const tweet = availableTweets[j] + const weight = tweet.weight ?? fallbackWeight + accumulatedWeight += weight + if (random <= accumulatedWeight) { + selectedTweets.push(tweet) + selectedIndex = j + break + } + } + + // Remove the selected tweet and update remaining weight + if (selectedIndex !== -1) { + const removedWeight = availableTweets[selectedIndex].weight ?? fallbackWeight + remainingWeight -= removedWeight + availableTweets.splice(selectedIndex, 1) + } + } + + return selectedTweets +} + +// Sort by weight (highest first), then take first 18 for static pages +export const topTweets = [...tweets].sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, 18) + export default tweets From 911d646bcb799a29bd4328e9584cbdd793ab382d Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:34:04 +0100 Subject: [PATCH 5/9] docs: add a new section about mcp in self-hosted (#39952) * docs: add a new section about mcp in self-hosted --- .../NavigationMenu.constants.ts | 4 + .../guides/self-hosting/enable-mcp.mdx | 147 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 apps/docs/content/guides/self-hosting/enable-mcp.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 2b7d9e57a0f3b..9b318b31690dc 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2639,6 +2639,10 @@ export const self_hosting: NavMenuConstant = { items: [ { name: 'Overview', url: '/guides/self-hosting' }, { name: 'Self-Hosting with Docker', url: '/guides/self-hosting/docker' }, + { + name: 'Configuration', + items: [{ name: 'Enabling MCP server', url: '/guides/self-hosting/enable-mcp' }], + }, { name: 'Auth Server', items: [ diff --git a/apps/docs/content/guides/self-hosting/enable-mcp.mdx b/apps/docs/content/guides/self-hosting/enable-mcp.mdx new file mode 100644 index 0000000000000..1549191d5bf94 --- /dev/null +++ b/apps/docs/content/guides/self-hosting/enable-mcp.mdx @@ -0,0 +1,147 @@ +--- +title: 'Enabling MCP Server Access' +description: 'Configure secure access to the MCP server in your self-hosted Supabase instance.' +subtitle: 'Configure secure access to the MCP server in your self-hosted Supabase instance.' +--- + +The MCP (Model Context Protocol) server in [self-hosted Supabase](/docs/guides/self-hosting/docker) runs behind the internal API. Currently, it does not offer OAuth 2.1 authentication, and is not intended to be exposed to the Internet. The corresponding API route has to be protected by restricting network connections from the outside. By default, all connections to the MCP server are denied. + +This guide explains how to securely enable access to your self-hosted MCP server. + +## Security considerations + + + +Do not allow connections to the self-hosted MCP server from the Internet. Only access it via: + +- A VPN connection to the server running the Studio container +- An SSH tunnel from your local machine + + + +## Accessing via SSH tunnel + +### Step 1: Determine the local IP address that will be used to access the MCP server + +When connecting via an SSH tunnel to the Studio Docker container, the source IP will be that of the Docker bridge gateway. You need to allow connections from this IP address. + +Determine the Docker bridge gateway IP on the host running your Supabase containers: + +```bash +docker inspect supabase-kong \ + --format '{{range .NetworkSettings.Networks}}{{println .Gateway}}{{end}}' +``` + +This command will output an IP address, e.g., `172.18.0.1`. + +### Step 2: Allow connections from the gateway IP + +Add the IP address you discovered to the Kong configuration by editing the following section in `./volumes/api/kong.yml`: + +1. Comment out the request-termination section +2. Remove the # symbols from the entire section starting with `- name: cors`, including `deny: []` +3. Add your local IP to the 'allow' list. +4. Your edited configuration should look like the example below. + +```yaml +## MCP endpoint - local access +- name: mcp + _comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)' + url: http://studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + paths: + - /mcp + plugins: + # Block access to /mcp by default + #- name: request-termination + # config: + # status_code: 403 + # message: "Access is forbidden." + # Enable local access (danger zone!) + # 1. Comment out the 'request-termination' section above + # 2. Uncomment the entire section below, including 'deny' + # 3. Add your local IPs to the 'allow' list + - name: cors + - name: ip-restriction + config: + allow: + - 127.0.0.1 + - ::1 + # Add your Docker bridge gateway IP below + - 172.18.0.1 + # Do not remove deny! + deny: [] +``` + +### Step 3: Restart API gateway + +After you've added the local IP address as above, restart the Kong container: + +```bash +docker compose restart kong +``` + +### Step 4: Create the SSH tunnel + +From your local machine, create an SSH tunnel to your Supabase host: + +```bash +ssh -L localhost:8080:localhost:8000 you@your-supabase-host +``` + +This command forwards local port `8080` to port `8000` on your Supabase host. + +### Step 5: Configure your MCP client + +Edit the settings for your MCP client and add the following to `"mcpServers": {}` or `"servers": {}`: + +```json +{ + "mcpServers": { + "supabase-self-hosted": { + "url": "http://localhost:8080/mcp" + } + } +} +``` + +### Step 6: Start using the self-hosted MCP server + +From your local machine, check that the MCP server is reachable: + +```bash +curl http://localhost:8080/mcp \ + -X POST \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "MCP-Protocol-Version: 2025-06-18" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": { + "elicitation": {} + }, + "clientInfo": { + "name": "test-client", + "title": "Test Client", + "version": "1.0.0" + } + } + }' +``` + +Start your MCP client (Claude Code, Cursor, etc.) and verify access to the MCP tools. For example, you can ask: "What is Supabase anon key? Use the Supabase MCP server tools." + +## Troubleshooting + +If you are unable to connect to the MCP server: + +1. Update Kong configuration file to the [latest version](https://github.com/supabase/supabase/blob/master/docker/volumes/api/kong.yml) and edit carefully +2. Confirm the Docker bridge gateway IP is correctly added in `./volumes/api/kong.yml` +3. Check Kong's logs for errors: `docker compose logs kong` +4. Make sure your SSH tunnel is active From 3bc91b2bccef792b96250390632f504330953b90 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 30 Oct 2025 10:15:18 +0100 Subject: [PATCH 6/9] connect dialog: add shared pooler to transaction pooler (#39981) fix shared pooler in transaction pooler + add link to docs in connect dialog --- .../components/interfaces/Connect/Connect.tsx | 2 +- .../interfaces/Connect/ConnectionPanel.tsx | 16 +- .../Connect/ConnectionParameters.tsx | 2 +- .../Connect/DatabaseConnectionString.tsx | 195 ++++++++++++------ .../studio/components/ui/DatabaseSelector.tsx | 4 +- 5 files changed, 154 insertions(+), 65 deletions(-) diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index 446c704d3769b..792661303b9d0 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -353,7 +353,7 @@ export const Connect = () => { {connectionTypes.length === 1 ? ` via ${connectionTypes[0].label.toLowerCase()}` : null} - Get the connection strings and environment variables for your app + Get the connection strings and environment variables for your app. diff --git a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx index da75b672c078e..abd8f1bad7224 100644 --- a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx @@ -15,6 +15,7 @@ import { Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, + Separator, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns' @@ -25,6 +26,7 @@ interface ConnectionPanelProps { badge?: string title: string description: string + contentFooter?: ReactNode connectionString: string ipv4Status: { type: 'error' | 'success' @@ -105,6 +107,7 @@ export const ConnectionPanel = ({ badge, title, description, + contentFooter, connectionString, ipv4Status, notice, @@ -123,16 +126,24 @@ export const ConnectionPanel = ({ const links = ipv4Status.links ?? [] + const isTransactionDedicatedPooler = type === 'transaction' && badge === 'Dedicated Pooler' + return (

{title}

- {!!badge && {badge}} + {!!badge && !isTransactionDedicatedPooler && {badge}}

{description}

+ {contentFooter}
+ {isTransactionDedicatedPooler && ( +
+ Using the Dedicated Pooler: +
+ )}
{fileTitle && } {type === 'transaction' && isSessionMode ? ( @@ -177,7 +188,6 @@ export const ConnectionPanel = ({ {parameters.length > 0 && } )} - {children}
{IS_PLATFORM && ( @@ -278,6 +288,8 @@ export const ConnectionPanel = ({ )}
+ {isTransactionDedicatedPooler && } + {children}
) diff --git a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx index d3654d9377cd9..8edc2c3335842 100644 --- a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx @@ -28,7 +28,7 @@ export const ConnectionParameters = ({ parameters }: ConnectionParametersProps) + + + handleCopy(selectedTab, 'transaction_pooler')} + /> +

+ Only recommended when your network does not support IPv6. Added latency + compared to dedicated pooler. +

+
+
+ )} + )} {selectedMethod === 'session' && IS_PLATFORM && ( @@ -587,14 +646,30 @@ const ConnectionStringMethodSelectItem = ({ poolerBadge, }: { method: ConnectionStringMethod - poolerBadge: string -}) => ( - -
-
{connectionStringMethodOptions[method].label}
-
- {connectionStringMethodOptions[method].description} + poolerBadge?: string +}) => { + const badges: ReactNode[] = [] + + if (method !== 'direct') { + badges.push(Shared Pooler) + } + if (poolerBadge === 'Dedicated Pooler') { + badges.push({poolerBadge}) + } + + return ( + +
+
+ {connectionStringMethodOptions[method].label} +
+
+ {connectionStringMethodOptions[method].description} +
+
+ {badges.map((badge) => badge)} +
-
- -) + + ) +} diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index ab61c00f67678..ccffbffc34959 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -38,6 +38,7 @@ interface DatabaseSelectorProps { onSelectId?: (id: string) => void // Optional callback onCreateReplicaClick?: () => void portal?: boolean + className?: string } const DatabaseSelector = ({ @@ -48,6 +49,7 @@ const DatabaseSelector = ({ buttonProps, onCreateReplicaClick = noop, portal = true, + className, }: DatabaseSelectorProps) => { const router = useRouter() const { ref: projectRef } = useParams() @@ -79,7 +81,7 @@ const DatabaseSelector = ({ return ( -
+
Source From 3e8baa2236ef19a3270398a86b81a48f223c8c29 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 30 Oct 2025 11:22:20 +0200 Subject: [PATCH 7/9] chore: Disable sending the Lock error to Sentry (#39999) Disable sending the Lock error to Sentry. --- apps/docs/lib/userAuth.ts | 7 +------ apps/studio/lib/gotrue.ts | 7 +------ packages/common/gotrue.ts | 3 +++ 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/docs/lib/userAuth.ts b/apps/docs/lib/userAuth.ts index 973c5eb8bb177..12fec885fe721 100644 --- a/apps/docs/lib/userAuth.ts +++ b/apps/docs/lib/userAuth.ts @@ -1,11 +1,6 @@ -import * as Sentry from '@sentry/nextjs' -import { gotrueClient, setCaptureException } from 'common' +import { gotrueClient } from 'common' import { useEffect } from 'react' -setCaptureException((e: any) => { - Sentry.captureException(e) -}) - export const auth = gotrueClient export async function getAccessToken() { diff --git a/apps/studio/lib/gotrue.ts b/apps/studio/lib/gotrue.ts index ab350710682dd..b51fdd29e7111 100644 --- a/apps/studio/lib/gotrue.ts +++ b/apps/studio/lib/gotrue.ts @@ -1,11 +1,6 @@ -import * as Sentry from '@sentry/nextjs' import type { JwtPayload } from '@supabase/supabase-js' import { getAccessToken, type User } from 'common/auth' -import { gotrueClient, setCaptureException } from 'common/gotrue' - -setCaptureException((e: any) => { - Sentry.captureException(e) -}) +import { gotrueClient } from 'common/gotrue' export const auth = gotrueClient export { getAccessToken } diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index 7a241cf4ce1ab..d2e4ef089419d 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -115,6 +115,9 @@ const logIndexedDB = (message: string, ...args: any[]) => { })() } +/** + * Reference to a function that captures exceptions for debugging purposes to be sent to Sentry. + */ let captureException: ((e: any) => any) | null = null export function setCaptureException(fn: typeof captureException) { From 5ba5acb4c7b0a67276fc6a50f474740f3ddcc95c Mon Sep 17 00:00:00 2001 From: Peter Soderberg Date: Thu, 30 Oct 2025 02:43:03 -0700 Subject: [PATCH 8/9] Add Peter Soderberg to humans.txt (#39984) enabled auto-merge, hope it works as intended --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 5a1292cb1b39d..75ce2d9c4693b 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -119,6 +119,7 @@ Paul Copplestone Pavel Borisov Paweł Gulbinowicz Peter Lyn +Peter Soderberg Qiao Han Rafael Chacón Raminder Singh From 02783c31794275c78b9d5cad3cd770c96c4aa378 Mon Sep 17 00:00:00 2001 From: Jean-Paul Argudo Date: Thu, 30 Oct 2025 10:43:13 +0100 Subject: [PATCH 9/9] Update humans.txt (#39982) Added my name to the list ;-) --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 75ce2d9c4693b..3c5239228cd28 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -74,6 +74,7 @@ Ignacio Dobronich Illia Basalaiev Inian P Ivan Vasilov +Jean-Paul Argudo Jeff Smick Jenny Kibiri Jess Shears