new file: contribute/plugins/utils/ast-utils.js

This commit is contained in:
Anderson 2026-01-26 03:46:43 +00:00 committed by GitHub
parent d7fc469a3d
commit 4448bccc9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
600 changed files with 121282 additions and 114 deletions

1
contribute/.nvmrc Normal file
View file

@ -0,0 +1 @@
20.19.1

1
contribute/.version Normal file
View file

@ -0,0 +1 @@
20

View file

@ -0,0 +1,53 @@
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
import globals from 'globals';
export default [
{ ignores: ['node_modules/**', 'dist/**', 'build/**', 'vite.config.js'] },
{
files: ['**/*.js', '**/*.jsx'],
plugins: { react, 'react-hooks': reactHooks, import: importPlugin },
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: { ecmaFeatures: { jsx: true } },
globals: { ...globals.browser, React: 'readonly', Intl: 'readonly' },
},
settings: {
react: { version: 'detect' },
'import/resolver': {
node: { extensions: ['.js', '.jsx'] },
alias: { map: [['@', './src']], extensions: ['.js', '.jsx'] },
},
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
...importPlugin.flatConfigs.recommended.rules,
// Non-critical rules - disabled since code works fine without them
'react/prop-types': 'off',
'react/no-unescaped-entities': 'off',
'react/display-name': 'off', // Non-critical, component works without displayName
'react/jsx-uses-react': 'off', // Not needed in React 17+, non-critical
'react/react-in-jsx-scope': 'off', // Not needed in React 17+, non-critical
'react/jsx-uses-vars': 'off', // Non-critical, code works fine
'react/jsx-no-comment-textnodes': 'off', // Non-critical, comments could be visible if put inside the JSX, most cases are just rendering text like '///'
'no-unused-vars': 'off', // Non-critical, code works fine with unused vars
'import/no-named-as-default': 'off', // Can cause runtime import errors, usually fine to leave as is
'import/no-named-as-default-member': 'off', // Can cause runtime import errors
// Critical rules that prevent runtime errors
'no-undef': 'error', // Undefined variables cause runtime errors
// Override recommended import rules for stricter checking
'import/no-self-import': 'error', // Extremely fast rule, breaking results in infinite loop/bundling error
// Disable expensive rules for performance
'import/no-cycle': 'off', // AI rarely makes this error, and the rule is very slow to run
},
},
{ files: ['tools/**/*.js', 'tailwind.config.js'], languageOptions: { globals: globals.node } },
];

14
contribute/index.html Normal file
View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="https://horizons-cdn.hostinger.com/1edbdcc3-df77-45f9-bc30-9f11766aa973/1df388aa94d930bbeb7ed03cb7a60888.png" />
<meta name="generator" content="Hostinger Horizons" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AeThex</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

10836
contribute/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

59
contribute/package.json Normal file
View file

@ -0,0 +1,59 @@
{
"name": "web-app",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --host :: --port 3000",
"build": "node tools/generate-llms.js || true && vite build",
"preview": "vite preview --host :: --port 3000"
},
"dependencies": {
"@babel/traverse": "^7.26.4",
"@emotion/is-prop-valid": "^1.2.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@supabase/supabase-js": "2.30.0",
"@tailwindcss/typography": "^0.5.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
"lucide-react": "^0.285.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-helmet": "^6.1.0",
"react-hot-toast": "^2.4.1",
"react-qrcode-logo": "^2.9.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@babel/generator": "^7.27.0",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@types/node": "^20.8.3",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16",
"eslint": "^8.57.1",
"eslint-config-react-app": "^7.0.1",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"terser": "^5.39.0",
"vite": "^4.4.5"
}
}

View file

@ -0,0 +1,430 @@
const ALLOWED_PARENT_ORIGINS = [
'https://horizons.hostinger.com',
'https://horizons.hostinger.dev',
'https://horizons-frontend-local.hostinger.dev',
'http://localhost:4000',
];
const IMPORTANT_STYLES = [
'display',
'position',
'flex-direction',
'justify-content',
'align-items',
'width',
'height',
'padding',
'margin',
'border',
'background-color',
'color',
'font-size',
'font-weight',
'font-family',
'border-radius',
'box-shadow',
'gap',
'grid-template-columns',
];
const PRIMARY_400_COLOR = '#7B68EE';
const TEXT_CONTEXT_MAX_LENGTH = 500;
const DATA_SELECTION_MODE_ENABLED_ATTRIBUTE = 'data-selection-mode-enabled';
const MESSAGE_TYPE_ENABLE_SELECTION_MODE = 'enableSelectionMode';
const MESSAGE_TYPE_DISABLE_SELECTION_MODE = 'disableSelectionMode';
let selectionModeEnabled = false;
let currentHoverElement = null;
let overlayDiv = null;
let selectedOverlayDiv = null;
let selectedElement = null;
function injectStyles() {
if (document.getElementById('selection-mode-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'selection-mode-styles';
style.textContent = `
#selection-mode-overlay {
position: absolute;
border: 2px dashed ${PRIMARY_400_COLOR};
pointer-events: none;
z-index: 999999;
}
#selection-mode-selected-overlay {
position: absolute;
border: 3px solid ${PRIMARY_400_COLOR};
pointer-events: none;
z-index: 999998;
}
`;
document.head.appendChild(style);
}
function getParentOrigin() {
if (
window.location.ancestorOrigins
&& window.location.ancestorOrigins.length > 0
) {
return window.location.ancestorOrigins[0];
}
if (document.referrer) {
try {
return new URL(document.referrer).origin;
} catch {
console.warn('[SELECTION MODE] Invalid referrer URL:', document.referrer);
}
}
return null;
}
/**
* Extract file path from React Fiber metadata (simplified - only for filePath)
* @param {*} node - DOM node
* @returns {string|null} - File path if found, null otherwise
*/
function getFilePathFromNode(node) {
const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber'));
if (!fiberKey) {
return null;
}
const fiber = node[fiberKey];
if (!fiber) {
return null;
}
// Traverse up the fiber tree to find source metadata
let currentFiber = fiber;
while (currentFiber) {
const source = currentFiber._debugSource
|| currentFiber.memoizedProps?.__source
|| currentFiber.pendingProps?.__source;
if (source?.fileName) {
return source.fileName;
}
currentFiber = currentFiber.return;
}
return null;
}
/**
* Generate a CSS selector path to uniquely identify the element
* @param {*} element
* @returns {string} CSS selector path
*/
function getPathToElement(element) {
const path = [];
let current = element;
let depth = 0;
const maxDepth = 20; // Prevent infinite loops
while (current && current.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
let selector = current.nodeName.toLowerCase();
if (current.id) {
selector += `#${current.id}`;
path.unshift(selector);
break; // ID is unique, stop here
}
if (current.className && typeof current.className === 'string') {
const classes = current.className.trim().split(/\s+/).filter(c => c.length > 0);
if (classes.length > 0) {
selector += `.${classes.join('.')}`;
}
}
if (current.parentElement) {
const siblings = Array.from(current.parentElement.children);
const sameTypeSiblings = siblings.filter(s => s.nodeName === current.nodeName);
if (sameTypeSiblings.length > 1) {
const index = sameTypeSiblings.indexOf(current) + 1;
selector += `:nth-of-type(${index})`;
}
}
path.unshift(selector);
current = current.parentElement;
depth++;
}
return path.join(' > ');
}
function getComputedStyles(element) {
const computedStyles = window.getComputedStyle(element);
return Object.fromEntries(IMPORTANT_STYLES.map((style) => {
const styleValue = computedStyles.getPropertyValue(style)?.trim();
return styleValue && styleValue !== 'none' && styleValue !== 'normal'
? [style, styleValue]
: null;
})
.filter(Boolean));
}
function extractDOMContext(element) {
if (!element) {
return null;
}
const textContent = element.textContent?.trim();
return {
outerHTML: element.outerHTML,
selector: getPathToElement(element),
attributes: (element.attributes && element.attributes.length > 0)
? Object.fromEntries(Array.from(element.attributes).map((attr) => [attr.name, attr.value]))
: {},
computedStyles: getComputedStyles(element),
textContent: (textContent && textContent.length > 0 && textContent.length < TEXT_CONTEXT_MAX_LENGTH)
? element.textContent?.trim()
: null
};
}
function createOverlay() {
if (overlayDiv) {
return;
}
injectStyles();
overlayDiv = document.createElement('div');
overlayDiv.id = 'selection-mode-overlay';
document.body.appendChild(overlayDiv);
}
function createSelectedOverlay() {
if (selectedOverlayDiv) {
return;
}
injectStyles();
selectedOverlayDiv = document.createElement('div');
selectedOverlayDiv.id = 'selection-mode-selected-overlay';
document.body.appendChild(selectedOverlayDiv);
}
function removeOverlay() {
if (overlayDiv && overlayDiv.parentNode) {
overlayDiv.parentNode.removeChild(overlayDiv);
overlayDiv = null;
}
if (selectedOverlayDiv && selectedOverlayDiv.parentNode) {
selectedOverlayDiv.parentNode.removeChild(selectedOverlayDiv);
selectedOverlayDiv = null;
}
}
function showOverlay(element) {
if (!overlayDiv) {
createOverlay();
}
const rect = element.getBoundingClientRect();
overlayDiv.style.left = `${rect.left + window.scrollX}px`;
overlayDiv.style.top = `${rect.top + window.scrollY}px`;
overlayDiv.style.width = `${rect.width}px`;
overlayDiv.style.height = `${rect.height}px`;
overlayDiv.style.display = 'block';
}
function showSelectedOverlay(element) {
if (!selectedOverlayDiv) {
createSelectedOverlay();
}
const rect = element.getBoundingClientRect();
selectedOverlayDiv.style.left = `${rect.left + window.scrollX}px`;
selectedOverlayDiv.style.top = `${rect.top + window.scrollY}px`;
selectedOverlayDiv.style.width = `${rect.width}px`;
selectedOverlayDiv.style.height = `${rect.height}px`;
selectedOverlayDiv.style.display = 'block';
}
function hideOverlay() {
if (overlayDiv) {
overlayDiv.style.display = 'none';
}
}
function handleMouseMove(event) {
if (!selectionModeEnabled) {
return;
}
const element = document.elementFromPoint(event.clientX, event.clientY);
if (!element) {
hideOverlay();
currentHoverElement = null;
return;
}
if (element === overlayDiv || element === selectedOverlayDiv) {
return;
}
// Only update if we're hovering a different element
if (currentHoverElement !== element) {
currentHoverElement = element;
// Show outline on the element
showOverlay(element);
}
}
function handleTouchStart(event) {
if (!selectionModeEnabled) {
return;
}
const touch = event.touches[0];
if (!touch) {
return;
}
const element = document.elementFromPoint(touch.clientX, touch.clientY);
if (!element) {
currentHoverElement = null;
return;
}
if (element === overlayDiv || element === selectedOverlayDiv) {
return;
}
currentHoverElement = element;
showOverlay(element);
}
function stripFilePath(filePath) {
if (!filePath) {
return filePath;
}
const publicHtmlIndex = filePath.indexOf('public_html/');
if (publicHtmlIndex !== -1) {
return filePath.substring(publicHtmlIndex + 'public_html/'.length);
}
return filePath;
}
function handleClick(event) {
if (!selectionModeEnabled) {
return;
}
if (!currentHoverElement) {
const element = document.elementFromPoint(event.clientX, event.clientY);
if (!element || element === overlayDiv || element === selectedOverlayDiv) {
return;
}
currentHoverElement = element;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const domContext = extractDOMContext(currentHoverElement);
if (!domContext) {
return;
}
selectedElement = currentHoverElement;
if (selectedElement) {
showSelectedOverlay(selectedElement);
}
// Extract file path from React Fiber (if available)
const filePath = getFilePathFromNode(currentHoverElement);
const strippedFilePath = filePath ? stripFilePath(filePath) : undefined;
// Send domContext and filePath to parent window
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
window.parent.postMessage(
{
type: 'elementSelected',
payload: {
filePath: strippedFilePath,
domContext,
},
},
parentOrigin,
);
}
}
function handleMouseLeave() {
if (!selectionModeEnabled) {
return;
}
hideOverlay();
currentHoverElement = null;
}
function enableSelectionMode() {
if (selectionModeEnabled) {
return;
}
selectionModeEnabled = true;
document.getElementById('root')?.setAttribute(DATA_SELECTION_MODE_ENABLED_ATTRIBUTE, 'true');
document.body.style.userSelect = 'none';
createOverlay();
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('touchstart', handleTouchStart, true);
document.addEventListener('click', handleClick, true);
document.addEventListener('mouseleave', handleMouseLeave, true);
}
function disableSelectionMode() {
if (!selectionModeEnabled) {
return;
}
selectionModeEnabled = false;
document.getElementById('root')?.removeAttribute(DATA_SELECTION_MODE_ENABLED_ATTRIBUTE);
document.body.style.userSelect = '';
hideOverlay();
removeOverlay();
currentHoverElement = null;
selectedElement = null;
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('touchstart', handleTouchStart, true);
document.removeEventListener('click', handleClick, true);
document.removeEventListener('mouseleave', handleMouseLeave, true);
}
window.addEventListener('message', (event) => {
if (event.data?.type === MESSAGE_TYPE_ENABLE_SELECTION_MODE) {
enableSelectionMode();
}
if (event.data?.type === MESSAGE_TYPE_DISABLE_SELECTION_MODE) {
disableSelectionMode();
}
});

View file

@ -0,0 +1,27 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
export default function selectionModePlugin() {
return {
name: 'vite:selection-mode',
apply: 'serve',
transformIndexHtml() {
const scriptPath = resolve(__dirname, 'selection-mode-script.js');
const scriptContent = readFileSync(scriptPath, 'utf-8');
return [
{
tag: 'script',
attrs: { type: 'module' },
children: scriptContent,
injectTo: 'body',
},
];
},
};
}

View file

@ -0,0 +1,279 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import generate from '@babel/generator';
import { parse } from '@babel/parser';
import traverseBabel from '@babel/traverse';
import {
isJSXIdentifier,
isJSXMemberExpression,
} from '@babel/types';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const VITE_PROJECT_ROOT = path.resolve(__dirname, '../..');
// Blacklist of components that should not be extracted (utility/non-visual components)
const COMPONENT_BLACKLIST = new Set([
'Helmet',
'HelmetProvider',
'Head',
'head',
'Meta',
'meta',
'Script',
'script',
'NoScript',
'noscript',
'Style',
'style',
'title',
'Title',
'link',
'Link',
]);
/**
* Validates that a file path is safe to access
* @param {string} filePath - Relative file path
* @returns {{ isValid: boolean, absolutePath?: string, error?: string }} - Object containing validation result
*/
export function validateFilePath(filePath) {
if (!filePath) {
return { isValid: false, error: 'Missing filePath' };
}
const absoluteFilePath = path.resolve(VITE_PROJECT_ROOT, filePath);
if (filePath.includes('..')
|| !absoluteFilePath.startsWith(VITE_PROJECT_ROOT)
|| absoluteFilePath.includes('node_modules')) {
return { isValid: false, error: 'Invalid path' };
}
if (!fs.existsSync(absoluteFilePath)) {
return { isValid: false, error: 'File not found' };
}
return { isValid: true, absolutePath: absoluteFilePath };
}
/**
* Parses a file into a Babel AST
* @param {string} absoluteFilePath - Absolute path to file
* @returns {object} Babel AST
*/
export function parseFileToAST(absoluteFilePath) {
const content = fs.readFileSync(absoluteFilePath, 'utf-8');
return parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true,
});
}
/**
* Finds a JSX opening element at a specific line and column
* @param {object} ast - Babel AST
* @param {number} line - Line number (1-indexed)
* @param {number} column - Column number (0-indexed for get-code-block, 1-indexed for apply-edit)
* @returns {object | null} Babel path to the JSX opening element
*/
export function findJSXElementAtPosition(ast, line, column) {
let targetNodePath = null;
let closestNodePath = null;
let closestDistance = Infinity;
const allNodesOnLine = [];
const visitor = {
JSXOpeningElement(path) {
const node = path.node;
if (node.loc) {
// Exact match (with tolerance for off-by-one column differences)
if (node.loc.start.line === line
&& Math.abs(node.loc.start.column - column) <= 1) {
targetNodePath = path;
path.stop();
return;
}
// Track all nodes on the same line
if (node.loc.start.line === line) {
allNodesOnLine.push({
path,
column: node.loc.start.column,
distance: Math.abs(node.loc.start.column - column),
});
}
// Track closest match on the same line for fallback
if (node.loc.start.line === line) {
const distance = Math.abs(node.loc.start.column - column);
if (distance < closestDistance) {
closestDistance = distance;
closestNodePath = path;
}
}
}
},
// Also check JSXElement nodes that contain the position
JSXElement(path) {
const node = path.node;
if (!node.loc) {
return;
}
// Check if this element spans the target line (for multi-line elements)
if (node.loc.start.line > line || node.loc.end.line < line) {
return;
}
// If we're inside this element's range, consider its opening element
if (!path.node.openingElement?.loc) {
return;
}
const openingLine = path.node.openingElement.loc.start.line;
const openingCol = path.node.openingElement.loc.start.column;
// Prefer elements that start on the exact line
if (openingLine === line) {
const distance = Math.abs(openingCol - column);
if (distance < closestDistance) {
closestDistance = distance;
closestNodePath = path.get('openingElement');
}
return;
}
// Handle elements that start before the target line
if (openingLine < line) {
const distance = (line - openingLine) * 100; // Penalize by line distance
if (distance < closestDistance) {
closestDistance = distance;
closestNodePath = path.get('openingElement');
}
}
},
};
traverseBabel.default(ast, visitor);
// Return exact match if found, otherwise return closest match if within reasonable distance
// Use larger threshold (50 chars) for same-line elements, 5 lines for multi-line elements
const threshold = closestDistance < 100 ? 50 : 500;
return targetNodePath || (closestDistance <= threshold ? closestNodePath : null);
}
/**
* Checks if a JSX element name is blacklisted
* @param {object} jsxOpeningElement - Babel JSX opening element node
* @returns {boolean} True if blacklisted
*/
function isBlacklistedComponent(jsxOpeningElement) {
if (!jsxOpeningElement || !jsxOpeningElement.name) {
return false;
}
// Handle JSXIdentifier (e.g., <Helmet>)
if (isJSXIdentifier(jsxOpeningElement.name)) {
return COMPONENT_BLACKLIST.has(jsxOpeningElement.name.name);
}
// Handle JSXMemberExpression (e.g., <React.Fragment>)
if (isJSXMemberExpression(jsxOpeningElement.name)) {
let current = jsxOpeningElement.name;
while (isJSXMemberExpression(current)) {
current = current.property;
}
if (isJSXIdentifier(current)) {
return COMPONENT_BLACKLIST.has(current.name);
}
}
return false;
}
/**
* Generates code from an AST node
* @param {object} node - Babel AST node
* @param {object} options - Generator options
* @returns {string} Generated code
*/
export function generateCode(node, options = {}) {
const generateFunction = generate.default || generate;
const output = generateFunction(node, options);
return output.code;
}
/**
* Generates a full source file from AST with source maps
* @param {object} ast - Babel AST
* @param {string} sourceFileName - Source file name for source map
* @param {string} originalCode - Original source code
* @returns {{code: string, map: object}} - Object containing generated code and source map
*/
export function generateSourceWithMap(ast, sourceFileName, originalCode) {
const generateFunction = generate.default || generate;
return generateFunction(ast, {
sourceMaps: true,
sourceFileName,
}, originalCode);
}
/**
* Extracts code blocks from a JSX element at a specific location
* @param {string} filePath - Relative file path
* @param {number} line - Line number
* @param {number} column - Column number
* @param {object} [domContext] - Optional DOM context to return on failure
* @returns {{success: boolean, filePath?: string, specificLine?: string, error?: string, domContext?: object}} - Object with metadata for LLM
*/
export function extractCodeBlocks(filePath, line, column, domContext) {
try {
// Validate file path
const validation = validateFilePath(filePath);
if (!validation.isValid) {
return { success: false, error: validation.error, domContext };
}
// Parse AST
const ast = parseFileToAST(validation.absolutePath);
// Find target node
const targetNodePath = findJSXElementAtPosition(ast, line, column);
if (!targetNodePath) {
return { success: false, error: 'Target node not found at specified line/column', domContext };
}
// Check if the target node is a blacklisted component
const isBlacklisted = isBlacklistedComponent(targetNodePath.node);
if (isBlacklisted) {
return {
success: true,
filePath,
specificLine: '',
};
}
// Get specific line code
const specificLine = generateCode(targetNodePath.parentPath?.node || targetNodePath.node);
return {
success: true,
filePath,
specificLine,
};
} catch (error) {
console.error('[ast-utils] Error extracting code blocks:', error);
return { success: false, error: 'Failed to extract code blocks', domContext };
}
}
/**
* Project root path
*/
export { VITE_PROJECT_ROOT };

View file

@ -0,0 +1,357 @@
// eslint-disable-next-line import/no-unresolved
import { POPUP_STYLES } from "./plugins/visual-editor/visual-editor-config.js";
const PLUGIN_APPLY_EDIT_API_URL = "/api/apply-edit";
const ALLOWED_PARENT_ORIGINS = [
"https://horizons.hostinger.com",
"https://horizons.hostinger.dev",
"https://horizons-frontend-local.hostinger.dev",
"http://localhost:4000",
];
let disabledTooltipElement = null;
let currentDisabledHoverElement = null;
let translations = {
disabledTooltipText: "This text can be changed only through chat.",
disabledTooltipTextImage: "This image can only be changed through chat.",
};
let areStylesInjected = false;
let globalEventHandlers = null;
let currentEditingInfo = null;
function injectPopupStyles() {
if (areStylesInjected) return;
const styleElement = document.createElement("style");
styleElement.id = "inline-editor-styles";
styleElement.textContent = POPUP_STYLES;
document.head.appendChild(styleElement);
areStylesInjected = true;
}
function findEditableElementAtPoint(event) {
let editableElement = event.target.closest("[data-edit-id]");
if (editableElement) {
return editableElement;
}
const elementsAtPoint = document.elementsFromPoint(
event.clientX,
event.clientY
);
const found = elementsAtPoint.find(
(el) => el !== event.target && el.hasAttribute("data-edit-id")
);
if (found) return found;
return null;
}
function findDisabledElementAtPoint(event) {
const direct = event.target.closest("[data-edit-disabled]");
if (direct) return direct;
const elementsAtPoint = document.elementsFromPoint(
event.clientX,
event.clientY
);
const found = elementsAtPoint.find(
(el) => el !== event.target && el.hasAttribute("data-edit-disabled")
);
if (found) return found;
return null;
}
function showPopup(targetElement, editId, currentContent, isImage = false) {
currentEditingInfo = { editId, targetElement };
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
const eventType = isImage ? "imageEditEnter" : "editEnter";
window.parent.postMessage(
{
type: eventType,
payload: { currentText: currentContent },
},
parentOrigin
);
}
}
function handleGlobalEvent(event) {
if (
!document.getElementById("root")?.getAttribute("data-edit-mode-enabled")
) {
return;
}
// Don't handle if selection mode is active
if (document.getElementById("root")?.getAttribute("data-selection-mode-enabled") === "true") {
return;
}
if (event.target.closest("#inline-editor-popup")) {
return;
}
const editableElement = findEditableElementAtPoint(event);
if (editableElement) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (event.type === "click") {
const editId = editableElement.getAttribute("data-edit-id");
if (!editId) {
console.warn("[INLINE EDITOR] Clicked element missing data-edit-id");
return;
}
const isImage = editableElement.tagName.toLowerCase() === "img";
let currentContent = "";
if (isImage) {
currentContent = editableElement.getAttribute("src") || "";
} else {
currentContent = editableElement.textContent || "";
}
showPopup(editableElement, editId, currentContent, isImage);
}
} else {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
}
function getParentOrigin() {
if (
window.location.ancestorOrigins &&
window.location.ancestorOrigins.length > 0
) {
return window.location.ancestorOrigins[0];
}
if (document.referrer) {
try {
return new URL(document.referrer).origin;
} catch (e) {
console.warn("Invalid referrer URL:", document.referrer);
}
}
return null;
}
async function handleEditSave(updatedText) {
const newText = updatedText
// Replacing characters that cause Babel parser to crash
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/{/g, "&#123;")
.replace(/}/g, "&#125;");
const { editId } = currentEditingInfo;
try {
const response = await fetch(PLUGIN_APPLY_EDIT_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
editId: editId,
newFullText: newText,
}),
});
const result = await response.json();
if (result.success) {
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
window.parent.postMessage(
{
type: "editApplied",
payload: {
editId: editId,
fileContent: result.newFileContent,
beforeCode: result.beforeCode,
afterCode: result.afterCode,
},
},
parentOrigin
);
} else {
console.error("Unauthorized parent origin:", parentOrigin);
}
} else {
console.error(
`[vite][visual-editor] Error saving changes: ${result.error}`
);
}
} catch (error) {
console.error(
`[vite][visual-editor] Error during fetch for ${editId}:`,
error
);
}
}
function createDisabledTooltip() {
if (disabledTooltipElement) return;
disabledTooltipElement = document.createElement("div");
disabledTooltipElement.id = "inline-editor-disabled-tooltip";
document.body.appendChild(disabledTooltipElement);
}
function showDisabledTooltip(targetElement, isImage = false) {
if (!disabledTooltipElement) createDisabledTooltip();
disabledTooltipElement.textContent = isImage
? translations.disabledTooltipTextImage
: translations.disabledTooltipText;
if (!disabledTooltipElement.isConnected) {
document.body.appendChild(disabledTooltipElement);
}
disabledTooltipElement.classList.add("tooltip-active");
const tooltipWidth = disabledTooltipElement.offsetWidth;
const tooltipHeight = disabledTooltipElement.offsetHeight;
const rect = targetElement.getBoundingClientRect();
// Ensures that tooltip is not off the screen with 5px margin
let newLeft = rect.left + window.scrollX + rect.width / 2 - tooltipWidth / 2;
let newTop = rect.bottom + window.scrollY + 5;
if (newLeft < window.scrollX) {
newLeft = window.scrollX + 5;
}
if (newLeft + tooltipWidth > window.innerWidth + window.scrollX) {
newLeft = window.innerWidth + window.scrollX - tooltipWidth - 5;
}
if (newTop + tooltipHeight > window.innerHeight + window.scrollY) {
newTop = rect.top + window.scrollY - tooltipHeight - 5;
}
if (newTop < window.scrollY) {
newTop = window.scrollY + 5;
}
disabledTooltipElement.style.left = `${newLeft}px`;
disabledTooltipElement.style.top = `${newTop}px`;
}
function hideDisabledTooltip() {
if (disabledTooltipElement) {
disabledTooltipElement.classList.remove("tooltip-active");
}
}
function handleDisabledElementHover(event) {
const isImage = event.currentTarget.tagName.toLowerCase() === "img";
showDisabledTooltip(event.currentTarget, isImage);
}
function handleDisabledElementLeave() {
hideDisabledTooltip();
}
function handleDisabledGlobalHover(event) {
const disabledElement = findDisabledElementAtPoint(event);
if (disabledElement) {
if (currentDisabledHoverElement !== disabledElement) {
currentDisabledHoverElement = disabledElement;
const isImage = disabledElement.tagName.toLowerCase() === "img";
showDisabledTooltip(disabledElement, isImage);
}
} else {
if (currentDisabledHoverElement) {
currentDisabledHoverElement = null;
hideDisabledTooltip();
}
}
}
function enableEditMode() {
// Don't enable if selection mode is active
if (document.getElementById("root")?.getAttribute("data-selection-mode-enabled") === "true") {
console.warn("[EDIT MODE] Cannot enable edit mode while selection mode is active");
return;
}
document
.getElementById("root")
?.setAttribute("data-edit-mode-enabled", "true");
injectPopupStyles();
if (!globalEventHandlers) {
globalEventHandlers = {
mousedown: handleGlobalEvent,
pointerdown: handleGlobalEvent,
click: handleGlobalEvent,
};
Object.entries(globalEventHandlers).forEach(([eventType, handler]) => {
document.addEventListener(eventType, handler, true);
});
}
document.addEventListener("mousemove", handleDisabledGlobalHover, true);
document.querySelectorAll("[data-edit-disabled]").forEach((el) => {
el.removeEventListener("mouseenter", handleDisabledElementHover);
el.addEventListener("mouseenter", handleDisabledElementHover);
el.removeEventListener("mouseleave", handleDisabledElementLeave);
el.addEventListener("mouseleave", handleDisabledElementLeave);
});
}
function disableEditMode() {
document.getElementById("root")?.removeAttribute("data-edit-mode-enabled");
hideDisabledTooltip();
if (globalEventHandlers) {
Object.entries(globalEventHandlers).forEach(([eventType, handler]) => {
document.removeEventListener(eventType, handler, true);
});
globalEventHandlers = null;
}
document.removeEventListener("mousemove", handleDisabledGlobalHover, true);
currentDisabledHoverElement = null;
document.querySelectorAll("[data-edit-disabled]").forEach((el) => {
el.removeEventListener("mouseenter", handleDisabledElementHover);
el.removeEventListener("mouseleave", handleDisabledElementLeave);
});
}
window.addEventListener("message", function (event) {
if (event.data?.type === "edit-save") {
handleEditSave(event.data?.payload?.newText);
}
if (event.data?.type === "enable-edit-mode") {
if (event.data?.translations) {
translations = { ...translations, ...event.data.translations };
}
enableEditMode();
}
if (event.data?.type === "disable-edit-mode") {
disableEditMode();
}
});

View file

@ -0,0 +1,137 @@
export const POPUP_STYLES = `
#inline-editor-popup {
width: 360px;
position: fixed;
z-index: 10000;
background: #161718;
color: white;
border: 1px solid #4a5568;
border-radius: 16px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
flex-direction: column;
gap: 10px;
display: none;
}
@media (max-width: 768px) {
#inline-editor-popup {
width: calc(100% - 20px);
}
}
#inline-editor-popup.is-active {
display: flex;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#inline-editor-popup.is-disabled-view {
padding: 10px 15px;
}
#inline-editor-popup textarea {
height: 100px;
padding: 4px 8px;
background: transparent;
color: white;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.42;
resize: none;
outline: none;
}
#inline-editor-popup .button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
}
#inline-editor-popup .popup-button {
border: none;
padding: 6px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 700;
height: 34px;
outline: none;
}
#inline-editor-popup .save-button {
background: #673de6;
color: white;
}
#inline-editor-popup .cancel-button {
background: transparent;
border: 1px solid #3b3d4a;
color: white;
&:hover {
background:#474958;
}
}
`;
export function getPopupHTMLTemplate(saveLabel, cancelLabel) {
return `
<textarea></textarea>
<div class="button-container">
<button class="popup-button cancel-button">${cancelLabel}</button>
<button class="popup-button save-button">${saveLabel}</button>
</div>
`;
}
export const EDIT_MODE_STYLES = `
#root[data-edit-mode-enabled="true"] [data-edit-id] {
cursor: pointer;
outline: 2px dashed #357DF9;
outline-offset: 2px;
min-height: 1em;
}
#root[data-edit-mode-enabled="true"] img[data-edit-id] {
outline-offset: -2px;
}
#root[data-edit-mode-enabled="true"] {
cursor: pointer;
}
#root[data-edit-mode-enabled="true"] [data-edit-id]:hover {
background-color: #357DF933;
outline-color: #357DF9;
}
@keyframes fadeInTooltip {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#inline-editor-disabled-tooltip {
display: none;
opacity: 0;
position: absolute;
background-color: #1D1E20;
color: white;
padding: 4px 8px;
border-radius: 8px;
z-index: 10001;
font-size: 14px;
border: 1px solid #3B3D4A;
max-width: 184px;
text-align: center;
}
#inline-editor-disabled-tooltip.tooltip-active {
display: block;
animation: fadeInTooltip 0.2s ease-out forwards;
}
`;

View file

@ -0,0 +1,32 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { EDIT_MODE_STYLES } from './visual-editor-config';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
export default function inlineEditDevPlugin() {
return {
name: 'vite:inline-edit-dev',
apply: 'serve',
transformIndexHtml() {
const scriptPath = resolve(__dirname, 'edit-mode-script.js');
const scriptContent = readFileSync(scriptPath, 'utf-8');
return [
{
tag: 'script',
attrs: { type: 'module' },
children: scriptContent,
injectTo: 'body'
},
{
tag: 'style',
children: EDIT_MODE_STYLES,
injectTo: 'head'
}
];
}
};
}

View file

@ -0,0 +1,365 @@
import path from 'path';
import { parse } from '@babel/parser';
import traverseBabel from '@babel/traverse';
import * as t from '@babel/types';
import fs from 'fs';
import {
validateFilePath,
parseFileToAST,
findJSXElementAtPosition,
generateCode,
generateSourceWithMap,
VITE_PROJECT_ROOT
} from '../utils/ast-utils.js';
const EDITABLE_HTML_TAGS = ["a", "Button", "button", "p", "span", "h1", "h2", "h3", "h4", "h5", "h6", "label", "Label", "img"];
function parseEditId(editId) {
const parts = editId.split(':');
if (parts.length < 3) {
return null;
}
const column = parseInt(parts.at(-1), 10);
const line = parseInt(parts.at(-2), 10);
const filePath = parts.slice(0, -2).join(':');
if (!filePath || isNaN(line) || isNaN(column)) {
return null;
}
return { filePath, line, column };
}
function checkTagNameEditable(openingElementNode, editableTagsList) {
if (!openingElementNode || !openingElementNode.name) return false;
const nameNode = openingElementNode.name;
// Check 1: Direct name (for <p>, <Button>)
if (nameNode.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.name)) {
return true;
}
// Check 2: Property name of a member expression (for <motion.h1>, check if "h1" is in editableTagsList)
if (nameNode.type === 'JSXMemberExpression' && nameNode.property && nameNode.property.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.property.name)) {
return true;
}
return false;
}
function validateImageSrc(openingNode) {
if (!openingNode || !openingNode.name || openingNode.name.name !== 'img') {
return { isValid: true, reason: null }; // Not an image, skip validation
}
const hasPropsSpread = openingNode.attributes.some(attr =>
t.isJSXSpreadAttribute(attr) &&
attr.argument &&
t.isIdentifier(attr.argument) &&
attr.argument.name === 'props'
);
if (hasPropsSpread) {
return { isValid: false, reason: 'props-spread' };
}
const srcAttr = openingNode.attributes.find(attr =>
t.isJSXAttribute(attr) &&
attr.name &&
attr.name.name === 'src'
);
if (!srcAttr) {
return { isValid: false, reason: 'missing-src' };
}
if (!t.isStringLiteral(srcAttr.value)) {
return { isValid: false, reason: 'dynamic-src' };
}
if (!srcAttr.value.value || srcAttr.value.value.trim() === '') {
return { isValid: false, reason: 'empty-src' };
}
return { isValid: true, reason: null };
}
export default function inlineEditPlugin() {
return {
name: 'vite-inline-edit-plugin',
enforce: 'pre',
transform(code, id) {
if (!/\.(jsx|tsx)$/.test(id) || !id.startsWith(VITE_PROJECT_ROOT) || id.includes('node_modules')) {
return null;
}
const relativeFilePath = path.relative(VITE_PROJECT_ROOT, id);
const webRelativeFilePath = relativeFilePath.split(path.sep).join('/');
try {
const babelAst = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true
});
let attributesAdded = 0;
traverseBabel.default(babelAst, {
enter(path) {
if (path.isJSXOpeningElement()) {
const openingNode = path.node;
const elementNode = path.parentPath.node; // The JSXElement itself
if (!openingNode.loc) {
return;
}
const alreadyHasId = openingNode.attributes.some(
(attr) => t.isJSXAttribute(attr) && attr.name.name === 'data-edit-id'
);
if (alreadyHasId) {
return;
}
// Condition 1: Is the current element tag type editable?
const isCurrentElementEditable = checkTagNameEditable(openingNode, EDITABLE_HTML_TAGS);
if (!isCurrentElementEditable) {
return;
}
const imageValidation = validateImageSrc(openingNode);
if (!imageValidation.isValid) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral('true')
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
let shouldBeDisabledDueToChildren = false;
// Condition 2: Does the element have dynamic or editable children
if (t.isJSXElement(elementNode) && elementNode.children) {
// Check if element has {...props} spread attribute - disable editing if it does
const hasPropsSpread = openingNode.attributes.some(attr => t.isJSXSpreadAttribute(attr)
&& attr.argument
&& t.isIdentifier(attr.argument)
&& attr.argument.name === 'props'
);
const hasDynamicChild = elementNode.children.some(child =>
t.isJSXExpressionContainer(child)
);
if (hasDynamicChild || hasPropsSpread) {
shouldBeDisabledDueToChildren = true;
}
}
if (!shouldBeDisabledDueToChildren && t.isJSXElement(elementNode) && elementNode.children) {
const hasEditableJsxChild = elementNode.children.some(child => {
if (t.isJSXElement(child)) {
return checkTagNameEditable(child.openingElement, EDITABLE_HTML_TAGS);
}
return false;
});
if (hasEditableJsxChild) {
shouldBeDisabledDueToChildren = true;
}
}
if (shouldBeDisabledDueToChildren) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral('true')
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
// Condition 3: Parent is non-editable if AT LEAST ONE child JSXElement is a non-editable type.
if (t.isJSXElement(elementNode) && elementNode.children && elementNode.children.length > 0) {
let hasNonEditableJsxChild = false;
for (const child of elementNode.children) {
if (t.isJSXElement(child)) {
if (!checkTagNameEditable(child.openingElement, EDITABLE_HTML_TAGS)) {
hasNonEditableJsxChild = true;
break;
}
}
}
if (hasNonEditableJsxChild) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral("true")
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
}
// Condition 4: Is any ancestor JSXElement also editable?
let currentAncestorCandidatePath = path.parentPath.parentPath;
while (currentAncestorCandidatePath) {
const ancestorJsxElementPath = currentAncestorCandidatePath.isJSXElement()
? currentAncestorCandidatePath
: currentAncestorCandidatePath.findParent(p => p.isJSXElement());
if (!ancestorJsxElementPath) {
break;
}
if (checkTagNameEditable(ancestorJsxElementPath.node.openingElement, EDITABLE_HTML_TAGS)) {
return;
}
currentAncestorCandidatePath = ancestorJsxElementPath.parentPath;
}
const line = openingNode.loc.start.line;
const column = openingNode.loc.start.column + 1;
const editId = `${webRelativeFilePath}:${line}:${column}`;
const idAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-id'),
t.stringLiteral(editId)
);
openingNode.attributes.push(idAttribute);
attributesAdded++;
}
}
});
if (attributesAdded > 0) {
const output = generateSourceWithMap(babelAst, webRelativeFilePath, code);
return { code: output.code, map: output.map };
}
return null;
} catch (error) {
console.error(`[vite][visual-editor] Error transforming ${id}:`, error);
return null;
}
},
// Updates source code based on the changes received from the client
configureServer(server) {
server.middlewares.use('/api/apply-edit', async (req, res, next) => {
if (req.method !== 'POST') return next();
let body = '';
req.on('data', chunk => { body += chunk.toString(); });
req.on('end', async () => {
let absoluteFilePath = '';
try {
const { editId, newFullText } = JSON.parse(body);
if (!editId || typeof newFullText === 'undefined') {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Missing editId or newFullText' }));
}
const parsedId = parseEditId(editId);
if (!parsedId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Invalid editId format (filePath:line:column)' }));
}
const { filePath, line, column } = parsedId;
// Validate file path
const validation = validateFilePath(filePath);
if (!validation.isValid) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: validation.error }));
}
absoluteFilePath = validation.absolutePath;
// Parse AST
const originalContent = fs.readFileSync(absoluteFilePath, 'utf-8');
const babelAst = parseFileToAST(absoluteFilePath);
// Find target node (note: apply-edit uses column+1)
const targetNodePath = findJSXElementAtPosition(babelAst, line, column + 1);
if (!targetNodePath) {
res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Target node not found by line/column', editId }));
}
const targetOpeningElement = targetNodePath.node;
const parentElementNode = targetNodePath.parentPath?.node;
const isImageElement = targetOpeningElement.name && targetOpeningElement.name.name === 'img';
let beforeCode = '';
let afterCode = '';
let modified = false;
if (isImageElement) {
// Handle image src attribute update
beforeCode = generateCode(targetOpeningElement);
const srcAttr = targetOpeningElement.attributes.find(attr =>
t.isJSXAttribute(attr) && attr.name && attr.name.name === 'src'
);
if (srcAttr && t.isStringLiteral(srcAttr.value)) {
srcAttr.value = t.stringLiteral(newFullText);
modified = true;
afterCode = generateCode(targetOpeningElement);
}
} else {
if (parentElementNode && t.isJSXElement(parentElementNode)) {
beforeCode = generateCode(parentElementNode);
parentElementNode.children = [];
if (newFullText && newFullText.trim() !== '') {
const newTextNode = t.jsxText(newFullText);
parentElementNode.children.push(newTextNode);
}
modified = true;
afterCode = generateCode(parentElementNode);
}
}
if (!modified) {
res.writeHead(409, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Could not apply changes to AST.' }));
}
const webRelativeFilePath = path.relative(VITE_PROJECT_ROOT, absoluteFilePath).split(path.sep).join('/');
const output = generateSourceWithMap(babelAst, webRelativeFilePath, originalContent);
const newContent = output.code;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
newFileContent: newContent,
beforeCode,
afterCode,
}));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error during edit application.' }));
}
});
});
}
};
}

View file

@ -0,0 +1,125 @@
export default function iframeRouteRestorationPlugin() {
return {
name: 'vite:iframe-route-restoration',
apply: 'serve',
transformIndexHtml() {
const script = `
const ALLOWED_PARENT_ORIGINS = [
"https://horizons.hostinger.com",
"https://horizons.hostinger.dev",
"https://horizons-frontend-local.hostinger.dev",
];
// Check to see if the page is in an iframe
if (window.self !== window.top) {
const STORAGE_KEY = 'horizons-iframe-saved-route';
const getCurrentRoute = () => location.pathname + location.search + location.hash;
const save = () => {
try {
const currentRoute = getCurrentRoute();
sessionStorage.setItem(STORAGE_KEY, currentRoute);
window.parent.postMessage({message: 'route-changed', route: currentRoute}, '*');
} catch {}
};
const replaceHistoryState = (url) => {
try {
history.replaceState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate', { state: history.state }));
return true;
} catch {}
return false;
};
const restore = () => {
try {
const saved = sessionStorage.getItem(STORAGE_KEY);
if (!saved) return;
if (!saved.startsWith('/')) {
sessionStorage.removeItem(STORAGE_KEY);
return;
}
const current = getCurrentRoute();
if (current !== saved) {
if (!replaceHistoryState(saved)) {
replaceHistoryState('/');
}
requestAnimationFrame(() => setTimeout(() => {
try {
const text = (document.body?.innerText || '').trim();
// If the restored route results in too little content, assume it is invalid and navigate home
if (text.length < 50) {
replaceHistoryState('/');
}
} catch {}
}, 1000));
}
} catch {}
};
const originalPushState = history.pushState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
save();
};
const originalReplaceState = history.replaceState;
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
save();
};
const getParentOrigin = () => {
if (
window.location.ancestorOrigins &&
window.location.ancestorOrigins.length > 0
) {
return window.location.ancestorOrigins[0];
}
if (document.referrer) {
try {
return new URL(document.referrer).origin;
} catch (e) {
console.warn("Invalid referrer URL:", document.referrer);
}
}
return null;
};
window.addEventListener('popstate', save);
window.addEventListener('hashchange', save);
window.addEventListener("message", function (event) {
const parentOrigin = getParentOrigin();
if (event.data?.type === "redirect-home" && parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
const saved = sessionStorage.getItem(STORAGE_KEY);
if(saved && saved !== '/') {
replaceHistoryState('/')
}
}
});
restore();
}
`;
return [
{
tag: 'script',
attrs: { type: 'module' },
children: script,
injectTo: 'head'
}
];
}
};
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -0,0 +1,19 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
</IfModule>
<IfModule mod_headers.c>
Header set X-Powered-By "Hostinger Horizons"
# Cache everything on CDN by default
Header set Cache-Control "public, s-maxage=604800, max-age=0"
# Cache in browser all assets
<If "%{REQUEST_URI} =~ m#^/assets/.*$#">
Header set Cache-Control "public, max-age=604800"
</If>
</IfModule>

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0D94EA;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0D41EA;stop-opacity:1" />
</linearGradient>
</defs>
<path fill="url(#grad1)" d="M128 0 L158.4 69.6 L233.6 69.6 L172.8 112.8 L203.2 182.4 L128 139.2 L52.8 182.4 L83.2 112.8 L22.4 69.6 L97.6 69.6 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 492 B

View file

@ -0,0 +1,16 @@
## Pages
- [- AeThex Careers`}](/jobapplication): No description available
- [- AeThex Careers`}](/jobdetail): No description available
- [About AeThex | Our Mission, Vision, and Team](/about): Learn about AeThex
- [AeThex | Engineering the Next Digital Epoch](/home): AeThex is a research and engineering collective building the foundational layers for the next generation of the internet. Discover our work in decentralized AI, next-gen protocols, and digital sovereignty.
- [Contact Us | AeThex](/contact): Get in touch with the AeThex team for inquiries, partnerships, or press via our contact form or official channels.
- [Get Involved | AeThex Collective](/getinvolved): Join the AeThex collective. Find volunteer opportunities, learn about our mission, and become a co-owner of the future of the internet.
- [My Applications - AeThex Careers](/myapplications): Track the status of your job applications with AeThex.
- [My Profile - AeThex](/myprofile): Manage your AeThex contributor profile.
- [My Registered Events - AeThex](/myevents): View and manage all the AeThex events you are registered for.
- [My Tickets | AeThex Contributor](/mytickets): View and manage your support tickets.
- [News & Press | AeThex](/news): The latest news, announcements, and press mentions from AeThex.
- [Notifications | AeThex Contributor](/notifications): View all your notifications.
- [Privacy Policy | AeThex](/privacypolicy): Read the AeThex privacy policy to understand how we collect, store, and use your personal data to provide our services.
- [Technology | AeThex](/technology): Explore the core technologies and research areas that power AeThex
- [Terms and Conditions | AeThex](/termsandconditions): Read the AeThex Terms and Conditions to understand the rules and guidelines for using our website and services.

109
contribute/src/App.jsx Normal file
View file

@ -0,0 +1,109 @@
import React from 'react';
import { AnimatePresence } from 'framer-motion';
import { Toaster } from '@/components/ui/toaster';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { useSite } from '@/contexts/SiteContext';
import LoadingScreen from '@/components/LoadingScreen';
import MaintenanceScreen from '@/components/MaintenanceScreen';
import AuthModal from '@/components/AuthModal';
import { Route, Routes, useLocation, Navigate } from 'react-router-dom';
import PageLayout from '@/components/PageLayout';
import HomePage from '@/pages/HomePage';
import AboutPage from '@/pages/AboutPage';
import TeamPage from '@/pages/TeamPage';
import TechnologyPage from '@/pages/TechnologyPage';
import NewsPage from '@/pages/NewsPage';
import ContactPage from '@/pages/ContactPage';
import PrivacyPolicyPage from '@/pages/PrivacyPolicyPage';
import TermsAndConditionsPage from '@/pages/TermsAndConditionsPage';
import MyProfilePage from '@/pages/MyProfilePage';
import GetInvolvedPage from '@/pages/GetInvolvedPage';
import MyTicketsPage from '@/pages/MyTicketsPage';
import NotificationsPage from '@/pages/NotificationsPage';
import ProtectedRoute from '@/components/ProtectedRoute';
import AdminLayout from '@/pages/admin/AdminLayout';
import AdminDashboardPage from '@/pages/admin/AdminDashboardPage';
import AdminContributorsPage from '@/pages/admin/AdminContributorsPage';
import AdminProjectsPage from '@/pages/admin/AdminProjectsPage';
import AdminTicketsPage from '@/pages/admin/AdminTicketsPage';
import AdminSettingsPage from '@/pages/admin/AdminSettingsPage';
const AppContent = () => {
const { showAuthModal, setShowAuthModal, loading: authLoading, profile } = useAuth();
const { siteConfig, loading: siteLoading } = useSite();
const location = useLocation();
if (authLoading || siteLoading) {
return <LoadingScreen />;
}
const isMaintenance = siteConfig?.system_status === 'maintenance';
const isAdmin = profile && ['admin', 'site_owner', 'oversee'].includes(profile.role);
if (isMaintenance && !isAdmin) {
return <MaintenanceScreen message={siteConfig.system_status_message} />;
}
return (
<>
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route element={<PageLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/get-involved" element={<GetInvolvedPage />} />
<Route path="/team" element={<TeamPage />} />
<Route path="/technology" element={<TechnologyPage />} />
<Route path="/news" element={<NewsPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/privacy" element={<PrivacyPolicyPage />} />
<Route path="/terms" element={<TermsAndConditionsPage />} />
<Route path="/profile" element={
<ProtectedRoute>
<MyProfilePage />
</ProtectedRoute>
} />
<Route path="/my-tickets" element={
<ProtectedRoute>
<MyTicketsPage />
</ProtectedRoute>
} />
<Route path="/notifications" element={
<ProtectedRoute>
<NotificationsPage />
</ProtectedRoute>
} />
</Route>
<Route path="/admin" element={
<ProtectedRoute adminOnly>
<AdminLayout />
</ProtectedRoute>
}>
<Route index element={<AdminDashboardPage />} />
<Route path="contributors" element={<AdminContributorsPage />} />
<Route path="projects" element={<AdminProjectsPage />} />
<Route path="tickets" element={<AdminTicketsPage />} />
<Route path="settings" element={<AdminSettingsPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Route>
</Routes>
</AnimatePresence>
<AnimatePresence>
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
</AnimatePresence>
<Toaster />
</>
);
};
function App() {
return <AppContent />;
}
export default App;

View file

@ -0,0 +1,18 @@
import React from 'react';
const AeThexLogo = ({ className, hideText }) => (
<div className={`flex items-center gap-3 ${className}`}>
<img
src="https://horizons-cdn.hostinger.com/1edbdcc3-df77-45f9-bc30-9f11766aa973/1df388aa94d930bbeb7ed03cb7a60888.png"
alt="AeThex Company Logo - Stylized Blue and Silver Symbol"
className="h-full w-auto"
/>
{!hideText && (
<span className="font-bold text-2xl tracking-tighter text-white font-mono">
AETHEX
</span>
)}
</div>
);
export default AeThexLogo;

View file

@ -0,0 +1,192 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { X, Loader2, Mail, Lock, Shield, ArrowRight, Github, Chrome } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { useToast } from '@/components/ui/use-toast';
const backdropVariants = {
visible: { opacity: 1 },
hidden: { opacity: 0 }
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.9, y: 50 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', damping: 25, stiffness: 200 }
},
exit: {
opacity: 0,
scale: 0.9,
y: 50,
transition: { duration: 0.2 }
}
};
const AuthModal = ({ onClose }) => {
const [view, setView] = useState('sign_in'); // 'sign_in', 'sign_up', 'forgot_password'
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const { signIn, signUp, signInWithGitHub, signInWithGoogle, sendPasswordResetEmail } = useAuth();
const { toast } = useToast();
const handleAuthAction = async (action, successMessage) => {
setLoading(true);
const { error } = await action();
setLoading(false);
if (!error) {
if (successMessage) {
toast(successMessage);
}
onClose();
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (view === 'sign_in') {
await handleAuthAction(() => signIn(email, password));
} else if (view === 'sign_up') {
await handleAuthAction(() => signUp(email, password), {
title: "Account Created",
description: "Please check your email to verify your account.",
});
} else if (view === 'forgot_password') {
await handleAuthAction(() => sendPasswordResetEmail(email), {
title: "Password Reset Email Sent",
description: "Please check your inbox for instructions.",
});
if(!loading) setView('sign_in');
}
};
const renderContent = () => {
switch (view) {
case 'forgot_password':
return (
<>
<h2 className="text-2xl font-bold text-white mb-2 text-center">Forgot Password?</h2>
<p className="text-gray-400 mb-6 text-center font-mono text-sm">Enter your email to receive a password reset link.</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input type="email" id="email-forgot" value={email} onChange={(e) => setEmail(e.target.value)} required className="pl-10" placeholder="Enter your email" />
</div>
<Button type="submit" className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-3 text-base flex items-center justify-center gap-2" disabled={loading}>
{loading ? <Loader2 className="animate-spin" /> : <>Send Reset Link <ArrowRight className="w-4 h-4" /></>}
</Button>
</form>
<p className="mt-6 text-center text-sm text-gray-400">
Remembered your password?{' '}
<button onClick={() => setView('sign_in')} className="font-medium text-primary hover:underline">
Sign In
</button>
</p>
</>
);
case 'sign_up':
case 'sign_in':
default:
return (
<>
<div className="flex justify-center mb-4">
<div className="h-16 w-16 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center">
<Shield className="w-8 h-8 text-primary" />
</div>
</div>
<h2 className="text-2xl font-bold text-center">
{view === 'sign_in' ? 'Welcome Back' : 'Become a Contributor'}
</h2>
<p className="text-gray-400 mb-6 text-center font-mono text-sm">
{view === 'sign_in' ? 'Sign in to your AeThex account' : 'Create an account to start contributing'}
</p>
<Button variant="outline" className="w-full mb-2 bg-transparent border-gray-700 hover:bg-gray-800" onClick={() => handleAuthAction(signInWithGitHub)} disabled={loading}>
<Github className="mr-2 h-4 w-4"/> Continue with GitHub
</Button>
<Button variant="outline" className="w-full mb-4 bg-transparent border-gray-700 hover:bg-gray-800" onClick={() => handleAuthAction(signInWithGoogle)} disabled={loading}>
<Chrome className="mr-2 h-4 w-4"/> Continue with Google
</Button>
<div className="relative flex py-2 items-center">
<div className="flex-grow border-t border-gray-700"></div>
<span className="flex-shrink mx-4 text-gray-500 text-xs font-mono">OR CONTINUE WITH EMAIL</span>
<div className="flex-grow border-t border-gray-700"></div>
</div>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="pl-10" placeholder="Enter your email" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="pl-10" placeholder="Enter your password" />
</div>
</div>
{view === 'sign_in' && (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox id="remember" />
<Label htmlFor="remember" className="text-sm font-normal text-gray-400">Remember me</Label>
</div>
<button onClick={() => setView('forgot_password')} type="button" className="text-sm text-primary hover:underline">Forgot password?</button>
</div>
)}
<Button type="submit" className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-3 text-base flex items-center justify-center gap-2" disabled={loading}>
{loading ? <Loader2 className="animate-spin" /> : <>{view === 'sign_in' ? 'Sign In' : 'Create Account'} <ArrowRight className="w-4 h-4" /></>}
</Button>
</form>
<p className="mt-6 text-center text-sm text-gray-400">
{view === 'sign_in' ? "Don't have an account?" : "Already have an account?"}{' '}
<button onClick={() => setView(view === 'sign_in' ? 'sign_up' : 'sign_in')} className="font-medium text-primary hover:underline">
{view === 'sign_in' ? 'Sign Up' : 'Sign In'}
</button>
</p>
</>
);
}
};
return (
<motion.div
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 font-mono"
onClick={onClose}
>
<motion.div
variants={modalVariants}
className="radial-gradient-background bg-[#0A071E] rounded-2xl border border-primary/20 max-w-md w-full shadow-2xl shadow-primary/10 text-white"
onClick={(e) => e.stopPropagation()}
>
<div className="p-8 relative">
<button onClick={onClose} className="absolute top-4 right-4 z-10 text-gray-400 hover:text-white p-1 rounded-full transition-colors">
<X className="w-5 h-5" />
</button>
{renderContent()}
</div>
</motion.div>
</motion.div>
);
};
export default AuthModal;

View file

@ -0,0 +1,17 @@
import React from 'react';
import { motion } from 'framer-motion';
const CallToAction = () => {
return (
<motion.h1
className='text-xl font-bold text-white leading-8 w-full'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
>
Let's turn your ideas into reality
</motion.h1>
);
};
export default CallToAction;

View file

@ -0,0 +1,103 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Calendar, MapPin, Star, ArrowRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { formatDate } from '@/lib/utils';
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" }
},
exit: {
opacity: 0,
y: -30,
transition: { duration: 0.3, ease: "easeIn" }
}
};
const EventCard = ({ event, onSelectEvent, getCategoryColor, getCategoryLabel }) => {
const eventUrl = `https://events.aethex.biz/event/${event.id}`;
return (
<motion.div
variants={cardVariants}
whileHover={{ y: -8, scale: 1.03 }}
transition={{ type: 'spring', stiffness: 300 }}
className="group relative"
layout
>
<div className="bg-gray-900/50 backdrop-blur-sm rounded-2xl border border-gray-800 overflow-hidden group-hover:border-primary/50 transition-all duration-300 h-full flex flex-col shadow-lg group-hover:shadow-primary/20">
<div className="relative h-48 overflow-hidden">
<div className={`absolute inset-0 bg-gradient-to-br ${getCategoryColor(event.category)} opacity-30 group-hover:opacity-50 transition-opacity duration-300`}></div>
<motion.img
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.4 }}
className="w-full h-full object-cover opacity-60 group-hover:opacity-80 transition-opacity duration-300"
alt={`${event.title} event banner`}
src="https://images.unsplash.com/photo-1540575467063-178a50c2df87?q=80&w=2070&auto=format&fit=crop"
/>
<div className="absolute top-4 left-4">
<span className="bg-black/60 backdrop-blur-sm text-white px-3 py-1 rounded-full text-xs font-medium border border-white/20">
{getCategoryLabel(event.category)}
</span>
</div>
{event.featured && (
<motion.div
initial={{ scale: 0, rotate: -45 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ delay: 0.5, type: 'spring' }}
className="absolute top-4 right-4 z-10"
>
<div className="bg-yellow-500/80 backdrop-blur-sm text-black px-3 py-1 rounded-full text-xs font-semibold flex items-center border border-yellow-300/50">
<Star className="w-3 h-3 mr-1" />
Featured
</div>
</motion.div>
)}
</div>
<div className="p-6 flex flex-col flex-grow">
<h2 className="text-lg font-bold text-white mb-2 group-hover:text-primary transition-colors">
{event.title}
</h2>
<p className="text-gray-400 mb-4 text-sm line-clamp-2 flex-grow">
{event.description}
</p>
<div className="space-y-2 mb-6 text-sm">
<div className="flex items-center text-gray-400">
<Calendar className="w-4 h-4 mr-2 text-primary/70" />
<span>{formatDate(event.date)}</span>
</div>
<div className="flex items-center text-gray-400">
<MapPin className="w-4 h-4 mr-2 text-primary/70" />
<span>{event.location}</span>
</div>
</div>
<div className="flex items-center justify-between mt-auto">
<div className="text-xl font-bold text-white">
{event.price === 0 ? 'Free' : `$${event.price}`}
</div>
<Button asChild className="bg-primary hover:bg-primary/90 text-white px-4 py-2 text-sm">
<a href={eventUrl} target="_blank" rel="noopener noreferrer">
<span>View Details</span>
<motion.div
className="inline-block"
whileHover={{ x: 4 }}
>
<ArrowRight className="w-4 h-4 ml-2" />
</motion.div>
</a>
</Button>
</div>
</div>
</div>
</motion.div>
);
};
export default EventCard;

View file

@ -0,0 +1,40 @@
import React from 'react';
import { motion } from 'framer-motion';
const EventCardSkeleton = () => {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="bg-gray-900/50 backdrop-blur-sm rounded-2xl border border-gray-800 overflow-hidden"
>
<div className="animate-pulse">
<div className="bg-gray-800 h-48 w-full"></div>
<div className="p-6">
<div className="h-5 bg-gray-700 rounded w-3/4 mb-4"></div>
<div className="h-3 bg-gray-700 rounded w-full mb-2"></div>
<div className="h-3 bg-gray-700 rounded w-5/6 mb-6"></div>
<div className="space-y-3 mb-6">
<div className="flex items-center">
<div className="h-4 w-4 bg-gray-700 rounded-full mr-2"></div>
<div className="h-3 bg-gray-700 rounded w-1/2"></div>
</div>
<div className="flex items-center">
<div className="h-4 w-4 bg-gray-700 rounded-full mr-2"></div>
<div className="h-3 bg-gray-700 rounded w-1/3"></div>
</div>
</div>
<div className="flex items-center justify-between mt-auto">
<div className="h-8 bg-gray-700 rounded w-1/4"></div>
<div className="h-10 bg-gray-700 rounded w-1/3"></div>
</div>
</div>
</div>
</motion.div>
);
};
export default EventCardSkeleton;

View file

@ -0,0 +1,162 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Calendar, Clock, MapPin, Users, X, Loader2, ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { formatDate, formatTime } from '@/lib/utils';
const backdropVariants = {
visible: { opacity: 1 },
hidden: { opacity: 0 }
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.9, y: 50 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', damping: 25, stiffness: 200 }
},
exit: {
opacity: 0,
scale: 0.9,
y: 50,
transition: { duration: 0.2 }
}
};
const EventDetailModal = ({ event, onClose, onRegister, onUnregister, isRegistered, getCategoryColor, getCategoryLabel, isLoading }) => {
const eventUrl = `https://events.aethex.biz/event/${event.id}`;
return (
<motion.div
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
variants={modalVariants}
className="bg-gray-900 rounded-2xl border border-gray-800 max-w-4xl w-full max-h-[90vh] overflow-y-auto shadow-2xl shadow-primary/10"
onClick={(e) => e.stopPropagation()}
>
<div className="relative">
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 bg-black/50 hover:bg-black/70 text-white p-2 rounded-full transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="relative h-64 overflow-hidden rounded-t-2xl">
<div className={`absolute inset-0 bg-gradient-to-br ${getCategoryColor(event.category)} opacity-30`}></div>
<motion.img
initial={{ scale: 1.1, opacity: 0.7 }}
animate={{ scale: 1, opacity: 0.6 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="w-full h-full object-cover"
alt={`${event.title} detailed view`}
src="https://images.unsplash.com/photo-1523580494863-6f3031224c94?q=80&w=2070&auto=format&fit=crop"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
className="absolute bottom-6 left-6"
>
<span className="bg-black/60 backdrop-blur-sm text-white px-4 py-2 rounded-full font-medium border border-white/20">
{getCategoryLabel(event.category)}
</span>
</motion.div>
</div>
<div className="p-8">
<div className="flex flex-col lg:flex-row gap-8">
<div className="flex-1">
<h1 className="text-3xl font-bold text-white mb-4">{event.title}</h1>
<p className="text-gray-300 mb-6 leading-relaxed">{event.full_description}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="space-y-4">
<h2 className="text-xl font-semibold text-white mb-3">Event Details</h2>
<div className="space-y-3">
<div className="flex items-center text-gray-300"><Calendar className="w-5 h-5 mr-3 text-primary" /><span>{formatDate(event.date)}</span></div>
<div className="flex items-center text-gray-300"><Clock className="w-5 h-5 mr-3 text-primary" /><span>{formatTime(event.time)}</span></div>
<div className="flex items-center text-gray-300"><MapPin className="w-5 h-5 mr-3 text-primary" /><span>{event.location}</span></div>
<div className="flex items-center text-gray-300"><Users className="w-5 h-5 mr-3 text-primary" /><span>{event.registered_count}/{event.capacity} registered</span></div>
</div>
</div>
<div className="space-y-4">
<h2 className="text-xl font-semibold text-white mb-3">Speakers</h2>
<ul className="space-y-2 list-disc list-inside text-gray-300">
{event.speakers && event.speakers.map((speaker, index) => <li key={index}>{speaker}</li>)}
</ul>
</div>
</div>
<div className="mb-8">
<h2 className="text-xl font-semibold text-white mb-4">Agenda</h2>
<div className="space-y-3">
{event.agenda && event.agenda.map((item, index) => (
<div key={index} className="flex items-start gap-4 p-4 bg-gray-900/50 rounded-lg border border-gray-800">
<div className="text-primary font-semibold min-w-[4rem]">{item.time}</div>
<div className="text-gray-300">{item.title}</div>
</div>
))}
</div>
</div>
</div>
<div className="lg:w-80">
<div className="bg-gray-900/50 rounded-xl p-6 border border-gray-800 sticky top-6">
<div className="text-center mb-6">
<div className="text-3xl font-bold text-white mb-2">{event.price === 0 ? 'Free' : `$${event.price}`}</div>
<div className="text-gray-400">per person</div>
</div>
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-400 mb-2">
<span>Availability</span>
<span>{event.capacity - event.registered_count} spots left</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-2.5">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(event.registered_count / event.capacity) * 100}%` }}
transition={{ duration: 1, ease: 'easeOut' }}
className="bg-primary h-2.5 rounded-full"
></motion.div>
</div>
</div>
{isRegistered ? (
<div className="space-y-3">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 text-center">
<div className="text-green-400 font-semibold"> Registered</div>
<div className="text-green-300 text-sm mt-1">You're all set!</div>
</div>
<Button onClick={() => onUnregister(event.id)} variant="outline" className="w-full border-red-500/30 text-red-400 hover:bg-red-500/10 hover:text-red-300" disabled={isLoading}>
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Cancel Registration
</Button>
</div>
) : (
<Button asChild className="w-full bg-primary hover:bg-primary/90 text-white py-3 text-base" disabled={event.registered_count >= event.capacity}>
<a href={eventUrl} target="_blank" rel="noopener noreferrer">
{event.registered_count >= event.capacity ? 'Event Full' : 'Register Now'}
<ExternalLink className="ml-2 h-4 w-4"/>
</a>
</Button>
)}
<div className="mt-4 text-xs text-gray-500 text-center">Registration and details are managed on our dedicated events platform.</div>
</div>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
);
};
export default EventDetailModal;

View file

@ -0,0 +1,120 @@
import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import EventCard from '@/components/EventCard';
import EventCardSkeleton from '@/components/EventCardSkeleton';
import { Search, SlidersHorizontal } from 'lucide-react';
import { useEvents } from '@/hooks/useEvents';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const EventList = ({ loading, filteredEvents, onSelectEvent, getCategoryColor, getCategoryLabel, setSearchTerm, setSelectedCategory }) => {
const { categories } = useEvents();
const [showFilters, setShowFilters] = useState(false);
return (
<>
<div className="mb-12 text-center">
<h1 className="text-4xl md:text-5xl font-bold text-white tracking-tight">AeThex Events</h1>
<p className="mt-4 text-lg text-gray-300 max-w-2xl mx-auto">
Explore our upcoming conferences, workshops, and networking opportunities.
</p>
</div>
<div className="mb-8 flex flex-col md:flex-row gap-4 items-center">
<div className="relative w-full md:flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
placeholder="Search events by title, topic, or location..."
className="w-full bg-gray-900/50 border border-gray-700/50 rounded-lg pl-12 pr-4 py-3 text-white focus:ring-2 focus:ring-primary focus:border-primary transition"
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="relative">
<button onClick={() => setShowFilters(!showFilters)} className="flex items-center gap-2 bg-gray-900/50 border border-gray-700/50 px-4 py-3 rounded-lg hover:border-primary/50 transition-colors">
<SlidersHorizontal className="w-5 h-5 text-gray-400" />
<span className="text-white">Filters</span>
</button>
<AnimatePresence>
{showFilters && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute right-0 mt-2 w-48 bg-gray-900 border border-gray-800 rounded-lg shadow-lg p-2 z-20"
>
{categories.map(category => (
<button
key={category}
onClick={() => {
setSelectedCategory(category);
setShowFilters(false);
}}
className="w-full text-left px-3 py-2 rounded text-sm text-gray-300 hover:bg-gray-800"
>
{getCategoryLabel(category)}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[...Array(6)].map((_, i) => (
<EventCardSkeleton key={i} />
))}
</div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
<AnimatePresence>
{filteredEvents.map((event) => (
<EventCard
key={event.id}
event={event}
onSelectEvent={onSelectEvent}
getCategoryColor={getCategoryColor}
getCategoryLabel={getCategoryLabel}
/>
))}
</AnimatePresence>
</motion.div>
)}
{filteredEvents.length === 0 && !loading && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-16"
>
<motion.div
className="text-6xl mb-4"
animate={{ rotate: [0, 10, -10, 10, 0], scale: [1, 1.1, 1] }}
transition={{ duration: 1, repeat: Infinity, repeatDelay: 2 }}
>
🔍
</motion.div>
<h3 className="text-2xl font-bold text-white mb-2">No events found</h3>
<p className="text-gray-400">Try adjusting your search or filter criteria.</p>
</motion.div>
)}
</>
);
};
export default EventList;

View file

@ -0,0 +1,160 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuGroup } from '@/components/ui/dropdown-menu';
import { User, LogOut, Shield, Mail, Loader2 } from 'lucide-react';
import AeThexLogo from './AeThexLogo';
import { Input } from './ui/input';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from './ui/use-toast';
const NewsletterForm = () => {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await supabase.from('newsletter_subscribers').insert({ email });
if (error) {
toast({
variant: 'destructive',
title: 'Subscription Failed',
description: error.code === '23505' ? 'This email is already subscribed.' : error.message,
});
} else {
toast({
title: 'Subscribed!',
description: 'Thank you for subscribing to the AeThex newsletter.',
});
setEmail('');
}
setLoading(false);
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
<p className="font-bold text-white tracking-wider">Stay Updated</p>
<p className="text-gray-400 text-sm mb-2">Join our newsletter for updates on our progress and new opportunities.</p>
<div className="flex w-full max-w-sm items-center space-x-2">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
className="bg-slate-800/50 border-slate-700"
/>
<Button type="submit" disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Subscribe'}
</Button>
</div>
</form>
);
};
const Footer = ({ onAuthClick }) => {
const { user, profile, signOut } = useAuth();
const isAdmin = profile && ['admin', 'site_owner', 'oversee'].includes(profile.role);
const avatarUrl = profile?.avatar_url || `https://api.dicebear.com/7.x/bottts/svg?seed=${user?.id}`;
const UserMenu = () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<img
className="h-10 w-10 rounded-full object-cover border-2 border-primary/50 hover:border-primary transition-colors cursor-pointer"
alt={profile?.username || 'User Avatar'}
src={avatarUrl}
/>
</motion.div>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-64 bg-slate-900/80 border-primary/20 text-white shadow-2xl shadow-primary/10 rounded-xl font-mono backdrop-blur-md mb-2"
align="end"
forceMount
>
<DropdownMenuLabel className="font-normal p-3">
<div className="flex flex-col space-y-1">
<p className="text-sm font-semibold leading-none">{profile?.full_name || profile?.username}</p>
<p className="text-xs leading-none text-gray-400">{profile?.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-primary/10" />
<DropdownMenuGroup>
<DropdownMenuItem asChild className="focus:bg-slate-800 focus:text-primary transition-colors cursor-pointer p-3">
<Link to="/profile">
<User className="mr-3 h-4 w-4" />
<span>My Profile</span>
</Link>
</DropdownMenuItem>
{isAdmin && (
<DropdownMenuItem asChild className="focus:bg-slate-800 focus:text-primary transition-colors cursor-pointer p-3">
<Link to="/admin">
<Shield className="mr-3 h-4 w-4" />
<span>Admin Panel</span>
</Link>
</DropdownMenuItem>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator className="bg-primary/10" />
<DropdownMenuItem onClick={signOut} className="text-red-400 focus:bg-red-900/40 focus:text-red-300 transition-colors cursor-pointer p-3">
<LogOut className="mr-3 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
return (
<footer className="bg-slate-950/80 backdrop-blur-lg border-t border-white/10 font-mono">
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="col-span-1">
<AeThexLogo className="h-8 mb-4" />
<p className="text-gray-400 text-sm">Engineering the foundational layers for the next digital epoch.</p>
</div>
<div>
<p className="font-bold text-white mb-4 tracking-wider">Company</p>
<ul className="space-y-2">
<li><Link to="/about" className="text-gray-400 hover:text-primary transition-colors">About Us</Link></li>
<li><Link to="/get-involved" className="text-gray-400 hover:text-primary transition-colors">Get Involved</Link></li>
<li><Link to="/team" className="text-gray-400 hover:text-primary transition-colors">Our Team</Link></li>
<li><Link to="/news" className="text-gray-400 hover:text-primary transition-colors">News</Link></li>
</ul>
</div>
<div>
<p className="font-bold text-white mb-4 tracking-wider">Resources</p>
<ul className="space-y-2">
<li><Link to="/technology" className="text-gray-400 hover:text-primary transition-colors">Technology</Link></li>
<li><Link to="/contact" className="text-gray-400 hover:text-primary transition-colors">Contact</Link></li>
<li><Link to="/privacy" className="text-gray-400 hover:text-primary transition-colors">Privacy Policy</Link></li>
<li><Link to="/terms" className="text-gray-400 hover:text-primary transition-colors">Terms & Conditions</Link></li>
</ul>
</div>
<div className="col-span-1">
<NewsletterForm />
</div>
</div>
<div className="mt-8 pt-8 border-t border-white/10 flex flex-col sm:flex-row justify-between items-center text-gray-500 text-sm">
<p>&copy; {new Date().getFullYear()} AeThex. All rights reserved.</p>
<div className="flex items-center gap-4 mt-4 sm:mt-0">
{user ? (
<UserMenu />
) : (
<Button onClick={onAuthClick} variant="outline" className="border-primary/50 text-primary/80 hover:bg-primary hover:text-slate-900 transition-colors duration-300 text-xs py-1 h-auto">
Contributor Login
</Button>
)}
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View file

@ -0,0 +1,133 @@
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import AeThexLogo from '@/components/AeThexLogo';
import NotificationBell from '@/components/NotificationBell';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { LayoutDashboard, User, LogOut, Ticket, Bell } from 'lucide-react';
const navLinks = [
{ name: 'Home', path: '/' },
{ name: 'About', path: '/about' },
{ name: 'Get Involved', path: '/get-involved' },
{ name: 'Team', path: '/team' },
{ name: 'Technology', path: '/technology' },
{ name: 'News', path: '/news' },
{ name: 'Contact', path: '/contact' },
];
const Header = () => {
const { user, profile, signOut, setShowAuthModal } = useAuth();
const isAdmin = profile && ['admin', 'site_owner', 'oversee'].includes(profile.role);
const avatarUrl = profile?.avatar_url || `https://api.dicebear.com/7.x/bottts/svg?seed=${user?.id}`;
return (
<motion.header
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ type: 'spring', stiffness: 50, delay: 0.2 }}
className="sticky top-4 inset-x-0 max-w-6xl mx-auto z-40"
>
<div className="flex items-center justify-between p-3 bg-black/50 backdrop-blur-lg rounded-2xl border border-white/10 shadow-2xl shadow-black/40">
<Link to="/" className="flex items-center gap-2">
<AeThexLogo className="h-8 w-auto" />
<span className="text-xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400 hidden sm:block">
AeThex
</span>
</Link>
<nav className="hidden lg:flex items-center gap-2">
{navLinks.map((link) => (
<NavLink
key={link.name}
to={link.path}
className={({ isActive }) =>
`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive
? 'text-white bg-primary/10'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`
}
>
{link.name}
</NavLink>
))}
</nav>
<div className="flex items-center gap-3">
{user ? (
<>
<NotificationBell />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 rounded-full p-0 flex-shrink-0 overflow-hidden">
<img
className="h-full w-full object-cover"
src={avatarUrl}
alt={profile?.username || 'User avatar'}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{profile?.username}</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{isAdmin && (
<DropdownMenuItem asChild>
<Link to="/admin">
<LayoutDashboard className="mr-2 h-4 w-4" />
<span>Admin Dashboard</span>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem asChild>
<Link to="/profile">
<User className="mr-2 h-4 w-4" />
<span>My Profile</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/notifications">
<Bell className="mr-2 h-4 w-4" />
<span>Notifications</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/my-tickets">
<Ticket className="mr-2 h-4 w-4" />
<span>My Tickets</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={signOut}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
) : (
<Button onClick={() => setShowAuthModal(true)}>
Sign In
</Button>
)}
</div>
</div>
</motion.header>
);
};
export default Header;

View file

@ -0,0 +1,31 @@
import React from 'react';
const HeroImage = () => {
return (
<div className="relative w-8 h-8 shrink-0" data-name="ic-sparkles">
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full"
>
<path
d="M11.787 9.5356C11.5053 8.82147 10.4947 8.82147 10.213 9.5356L8.742 13.2654C8.65601 13.4834 8.48343 13.656 8.2654 13.742L4.5356 15.213C3.82147 15.4947 3.82147 16.5053 4.5356 16.787L8.2654 18.258C8.48343 18.344 8.65601 18.5166 8.742 18.7346L10.213 22.4644C10.4947 23.1785 11.5053 23.1785 11.787 22.4644L13.258 18.7346C13.344 18.5166 13.5166 18.344 13.7346 18.258L17.4644 16.787C18.1785 16.5053 18.1785 15.4947 17.4644 15.213L13.7346 13.742C13.5166 13.656 13.344 13.4834 13.258 13.2654L11.787 9.5356Z"
fill="white"
/>
<path
d="M23.5621 2.38257C23.361 1.87248 22.639 1.87248 22.4379 2.38257L21.3871 5.04671C21.3257 5.20245 21.2024 5.32572 21.0467 5.38714L18.3826 6.43787C17.8725 6.63904 17.8725 7.36096 18.3826 7.56214L21.0467 8.61286C21.2024 8.67428 21.3257 8.79755 21.3871 8.95329L22.4379 11.6174C22.639 12.1275 23.361 12.1275 23.5621 11.6174L24.6129 8.95329C24.6743 8.79755 24.7976 8.67428 24.9533 8.61286L27.6174 7.56214C28.1275 7.36096 28.1275 6.63904 27.6174 6.43787L24.9533 5.38714C24.7976 5.32572 24.6743 5.20245 24.6129 5.04671L23.5621 2.38257Z"
fill="white"
/>
<path
d="M23.3373 22.2295C23.2166 21.9235 22.7834 21.9235 22.6627 22.2295L22.0323 23.828C21.9954 23.9215 21.9215 23.9954 21.828 24.0323L20.2295 24.6627C19.9235 24.7834 19.9235 25.2166 20.2295 25.3373L21.828 25.9677C21.9215 26.0046 21.9954 26.0785 22.0323 26.172L22.6627 27.7705C22.7834 28.0765 23.2166 28.0765 23.3373 27.7705L23.9677 26.172C24.0046 26.0785 24.0785 26.0046 24.172 25.9677L25.7705 25.3373C26.0765 25.2166 26.0765 24.7834 25.7705 24.6627L24.172 24.0323C24.0785 23.9954 24.0046 23.9215 23.9677 23.828L23.3373 22.2295Z"
fill="white"
/>
</svg>
</div>
);
};
export default HeroImage;

View file

@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import AeThexLogo from '@/components/AeThexLogo';
const BinaryDigit = () => {
const [position, setPosition] = useState({
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
});
const duration = Math.random() * 5 + 5; // 5 to 10 seconds
return (
<motion.span
className="absolute text-primary/20 text-xs font-mono"
style={{ ...position }}
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0] }}
transition={{ duration, repeat: Infinity, ease: "linear" }}
>
{Math.round(Math.random())}
</motion.span>
);
};
const DataFallBar = ({ i }) => {
const height = Math.random() * 24 + 8;
const duration = Math.random() * 0.5 + 0.8;
return (
<motion.div
className="w-1.5 bg-gradient-to-t from-primary/50 to-purple-600/50"
style={{ height: `${height}px`, opacity: 0 }}
animate={{
y: [0, 48],
opacity: [1, 0],
}}
transition={{
duration,
repeat: Infinity,
ease: 'linear',
delay: i * 0.15 + Math.random() * 0.5,
}}
/>
);
};
const LoadingScreen = () => {
const [progress, setProgress] = useState(0);
const [text, setText] = useState("Initializing AeThex OS...");
useEffect(() => {
let currentProgress = 0;
const interval = setInterval(() => {
currentProgress += 1;
if (currentProgress > 98) {
currentProgress = 98;
}
setProgress(currentProgress);
if (currentProgress > 60 && currentProgress < 80) {
setText("Calibrating systems...");
} else if (currentProgress >= 80) {
setText("Booting interface...");
}
}, 40);
return () => clearInterval(interval);
}, []);
return (
<div className="flex items-center justify-center min-h-screen bg-[#06080d] overflow-hidden relative">
<AnimatePresence>
<>
{Array.from({ length: 30 }).map((_, i) => (
<BinaryDigit key={i} />
))}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center gap-8 z-10 w-full max-w-sm px-4"
>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="relative"
>
<div className="absolute -inset-2 bg-primary/20 rounded-[28px] blur-xl opacity-75"></div>
<div className="relative w-24 h-24 bg-slate-900/50 rounded-3xl flex items-center justify-center border border-primary/20 backdrop-blur-sm p-4">
<AeThexLogo className="h-full" hideText />
</div>
</motion.div>
<div className="flex justify-center items-start h-12 w-24 overflow-hidden relative -mt-4">
<div className="absolute top-0 flex justify-center items-start gap-1">
{Array.from({ length: 9 }).map((_, i) => (
<DataFallBar key={i} i={i} />
))}
</div>
</div>
<div className="w-full max-w-xs -mt-4">
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-primary to-purple-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.1, ease: "linear" }}
/>
</div>
<p className="text-center text-sm text-gray-400 font-mono mt-2 tracking-widest">{progress}%</p>
</div>
<div className="text-center font-mono h-16">
<p className="text-lg text-primary">{text}</p>
<p className="text-sm text-gray-500 mt-1">Please wait while we prepare your experience...</p>
</div>
</motion.div>
</>
</AnimatePresence>
</div>
);
};
export default LoadingScreen;

View file

@ -0,0 +1,39 @@
import React from 'react';
import { motion } from 'framer-motion';
import AeThexLogo from '@/components/AeThexLogo';
import { HardDrive } from 'lucide-react';
const MaintenanceScreen = ({ message }) => {
const defaultMessage = "We're currently performing scheduled maintenance. We'll be back online shortly. Thank you for your patience!";
return (
<div className="flex items-center justify-center min-h-screen bg-[#06080d] text-white font-sans p-4">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-center max-w-2xl"
>
<div className="flex justify-center mb-8">
<div className="relative w-24 h-24 bg-slate-900/50 rounded-3xl flex items-center justify-center border border-primary/20 backdrop-blur-sm p-4">
<div className="absolute -inset-2 bg-primary/20 rounded-[28px] blur-xl opacity-75"></div>
<AeThexLogo className="h-full" hideText />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-primary mb-4 flex items-center justify-center gap-4">
<HardDrive className="w-10 h-10" />
Under Maintenance
</h1>
<p className="text-lg text-gray-300 mb-2">
Our site is temporarily unavailable.
</p>
<p className="text-md text-gray-400">
{message || defaultMessage}
</p>
</motion.div>
</div>
);
};
export default MaintenanceScreen;

View file

@ -0,0 +1,130 @@
import React from 'react';
import { Bell, MessageSquare, Briefcase, UserPlus } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuFooter
} from "@/components/ui/dropdown-menu";
import { Button } from './ui/button';
import { useNotifications } from '@/contexts/NotificationContext';
import { timeAgo } from '@/lib/utils';
import { Link, useNavigate } from 'react-router-dom';
const NotificationIcon = ({ type }) => {
switch (type) {
case 'new_message':
return <MessageSquare className="w-5 h-5 text-blue-400" />;
case 'new_job_application':
return <Briefcase className="w-5 h-5 text-green-400" />;
case 'user_joined':
return <UserPlus className="w-5 h-5 text-purple-400" />;
default:
return <Bell className="w-5 h-5 text-gray-400" />;
}
}
const getNotificationDetails = (notification) => {
const { type, data } = notification;
switch(type) {
case 'new_message':
return {
text: <p>New message from <strong>{data.sender_username}</strong>: "{data.message_preview}..."</p>,
link: `/messages/${data.conversation_id}`
};
case 'new_job_application':
return {
text: <p>New application for <strong>{data.job_title}</strong> from {data.applicant_username}.</p>,
link: `/admin/applications/${data.application_id}`
};
case 'user_joined':
return {
text: <p><strong>{data.username}</strong> just joined the platform!</p>,
link: `/profile/${data.user_id}`
};
default:
return {
text: <p>{data.message || 'New notification'}</p>,
link: '#'
};
}
};
const NotificationItem = ({ notification }) => {
const { markAsRead } = useNotifications();
const navigate = useNavigate();
const { text, link } = getNotificationDetails(notification);
const handleClick = (e) => {
e.preventDefault();
markAsRead(notification.id);
navigate(link);
}
return (
<DropdownMenuItem
className={`p-3 flex items-start gap-3 cursor-pointer focus:bg-gray-800 ${!notification.is_read ? 'bg-primary/10' : ''}`}
onSelect={handleClick}
>
<div className="flex-shrink-0 mt-1">
<NotificationIcon type={notification.type} />
</div>
<div className="w-full">
<div className="text-sm text-gray-300">{text}</div>
<p className="text-xs text-gray-500 mt-1">{timeAgo(notification.created_at)}</p>
</div>
</DropdownMenuItem>
)
}
const NotificationBell = () => {
const { notifications, unreadCount, markAllAsRead } = useNotifications();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 p-0 rounded-full flex-shrink-0">
<Bell className="h-5 w-5 text-gray-400" />
{unreadCount > 0 && (
<span className="absolute top-1 right-1 flex h-4 w-4">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-4 w-4 bg-red-500 text-white text-xs items-center justify-center">{unreadCount}</span>
</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 md:w-96 bg-gray-900 border-white/10 text-white shadow-2xl">
<div className="flex items-center justify-between p-3">
<DropdownMenuLabel className="p-0 text-base font-semibold">Notifications</DropdownMenuLabel>
{unreadCount > 0 && (
<Button variant="link" className="p-0 h-auto text-sm text-primary" onClick={(e) => { e.stopPropagation(); markAllAsRead(); }}>Mark all as read</Button>
)}
</div>
<DropdownMenuSeparator className="bg-white/10"/>
<div className="max-h-80 overflow-y-auto custom-scrollbar">
{notifications.length > 0 ? (
notifications.slice(0, 5).map(notification => (
<NotificationItem key={notification.id} notification={notification} />
))
) : (
<div className="text-center text-gray-500 p-8">
<p>No new notifications</p>
</div>
)}
</div>
<DropdownMenuSeparator className="bg-white/10"/>
<DropdownMenuFooter className="p-1">
<Link to="/notifications" className="w-full">
<Button variant="ghost" className="w-full text-primary">View All Notifications</Button>
</Link>
</DropdownMenuFooter>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default NotificationBell;

View file

@ -0,0 +1,36 @@
import React from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { motion } from 'framer-motion';
import Header from '@/components/Header';
import { useAuth } from '@/contexts/SupabaseAuthContext';
const PageLayout = () => {
const location = useLocation();
const { setShowAuthModal } = useAuth();
const handleAuthClick = () => {
if (setShowAuthModal) {
setShowAuthModal(true);
}
};
return (
<div className="min-h-screen bg-slate-950 text-white flex flex-col radial-gradient-background">
<Header onAuthClick={handleAuthClick} />
<main className="flex-1 w-full pt-8 pb-16">
<motion.div
key={location.key}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5, ease: 'easeInOut' }}
className="container mx-auto px-4"
>
<Outlet context={{ onAuthClick: handleAuthClick }} />
</motion.div>
</main>
</div>
);
};
export default PageLayout;

View file

@ -0,0 +1,144 @@
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { X, Copy, Check, Loader2 } from 'lucide-react';
import { QRCode } from 'react-qrcode-logo';
import AeThexLogo from './AeThexLogo';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { supabase } from '@/lib/customSupabaseClient';
import toast from 'react-hot-toast';
const backdropVariants = {
visible: { opacity: 1 },
hidden: { opacity: 0 }
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.9, y: 50 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', damping: 25, stiffness: 200 }
},
exit: {
opacity: 0,
scale: 0.9,
y: 50,
transition: { duration: 0.2 }
}
};
const PassportModal = ({ onClose }) => {
const { user } = useAuth();
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
useEffect(() => {
const fetchProfile = async () => {
if (user) {
setLoading(true);
const { data, error } = await supabase
.from('profiles')
.select('username, avatar_url, aethex_passport_id, created_at')
.eq('id', user.id)
.single();
if (error) {
console.error('Error fetching profile:', error);
toast.error("Failed to load Passport data.");
} else {
setProfile(data);
}
setLoading(false);
}
};
fetchProfile();
}, [user]);
const handleCopy = () => {
if (profile?.aethex_passport_id) {
navigator.clipboard.writeText(profile.aethex_passport_id);
setCopied(true);
toast.success("Passport ID Copied!");
setTimeout(() => setCopied(false), 2000);
}
};
return (
<motion.div
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
variants={modalVariants}
className="relative bg-gray-900 rounded-2xl border border-primary/20 w-full max-w-sm shadow-2xl shadow-primary/10 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={onClose}
className="absolute top-4 right-4 z-20 bg-black/50 hover:bg-black/70 text-white p-2 rounded-full transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="relative p-8 bg-grid-pattern">
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900"></div>
<div className="relative z-10 flex flex-col items-center text-center">
<img
src={profile?.avatar_url || 'https://i.pravatar.cc/150?u=a042581f4e29026704d'}
alt="User Avatar"
className="w-24 h-24 rounded-full object-cover border-4 border-primary shadow-lg"
/>
<h2 className="text-2xl font-bold text-white mt-4">{profile?.username || 'Loading...'}</h2>
<p className="text-sm text-gray-400">{user?.email}</p>
</div>
</div>
<div className="p-8">
{loading ? (
<div className="flex justify-center items-center h-48">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : (
<div className="flex flex-col items-center space-y-6">
<div className="bg-white p-3 rounded-lg shadow-md">
<QRCode
value={profile?.aethex_passport_id || 'no-id'}
size={150}
logoImage="/aethex-icon.svg"
logoWidth={40}
logoHeight={40}
qrStyle="dots"
eyeRadius={5}
/>
</div>
<div className="text-center w-full">
<p className="text-xs text-gray-500 uppercase font-semibold">Passport ID</p>
<div className="flex items-center justify-center mt-1 bg-gray-800 rounded-lg px-3 py-2 border border-gray-700">
<p className="text-sm text-primary font-mono truncate">{profile?.aethex_passport_id}</p>
<button onClick={handleCopy} className="ml-3 text-gray-400 hover:text-white">
{copied ? <Check className="w-4 h-4 text-green-500"/> : <Copy className="w-4 h-4" />}
</button>
</div>
</div>
<div className="text-center">
<p className="text-xs text-gray-500 uppercase font-semibold">Member Since</p>
<p className="text-sm text-white mt-1">{profile ? new Date(profile.created_at).toLocaleDateString() : '...'}</p>
</div>
</div>
)}
</div>
<div className="bg-gray-900/50 px-8 py-4 border-t border-gray-800 flex justify-center items-center">
<AeThexLogo />
</div>
</motion.div>
</motion.div>
);
};
export default PassportModal;

View file

@ -0,0 +1,31 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import LoadingScreen from '@/components/LoadingScreen';
const ProtectedRoute = ({ children, adminOnly = false }) => {
const { user, profile, loading } = useAuth();
const location = useLocation();
if (loading) {
return <LoadingScreen />;
}
if (!user) {
return <Navigate to="/" state={{ from: location }} replace />;
}
if (adminOnly) {
if (!profile) {
return <LoadingScreen />;
}
if (!['admin', 'site_owner', 'oversee'].includes(profile?.role)) {
return <Navigate to="/" state={{ from: location }} replace />;
}
}
return children;
};
export default ProtectedRoute;

View file

@ -0,0 +1,17 @@
import React from 'react';
import { motion } from 'framer-motion';
const WelcomeMessage = () => {
return (
<motion.p
className='text-sm text-white leading-5 w-full'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.8 }}
>
Write in the chat what you want to create.
</motion.p>
);
};
export default WelcomeMessage;

View file

@ -0,0 +1,43 @@
import React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b border-white/10", className)} {...props} />
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180 text-lg",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

@ -0,0 +1,36 @@
import React from "react"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
success:
"border-transparent bg-green-500 text-white hover:bg-green-500/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
...props
}) {
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,47 @@
import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import React from 'react';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 relative overflow-hidden group',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90 hover:scale-105 transform',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90 hover:scale-105 transform',
outline:
'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8 text-base',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
});
Button.displayName = 'Button';
export { Button, buttonVariants };

View file

@ -0,0 +1,60 @@
import React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border border-white/10 bg-gray-950/20 text-white shadow-2xl shadow-black/40 backdrop-blur-lg transition-all duration-300 hover:border-primary/50 hover:shadow-primary/10',
className,
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400', className)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-gray-400', className)} {...props} />
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View file

@ -0,0 +1,22 @@
import React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View file

@ -0,0 +1,94 @@
import React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-800 bg-gray-900/90 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 data-[state=open]:text-slate-500 dark:ring-offset-slate-950 dark:focus:ring-slate-300 dark:data-[state=open]:bg-slate-800 dark:data-[state=open]:text-slate-400">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-slate-500 dark:text-slate-400", className)}
{...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,172 @@
import React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-800 dark:data-[state=open]:bg-slate-800",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
className
)}
{...props} />
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
className
)}
{...props} />
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
inset && "pl-8",
className
)}
{...props} />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
className
)}
checked={checked}
{...props}>
<span
className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
className
)}
{...props}>
<span
className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props} />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800", className)}
{...props} />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}) => {
return (
(<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} />)
);
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
const DropdownMenuFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("p-1", className)}
{...props}
/>
));
DropdownMenuFooter.displayName = "DropdownMenuFooter";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuFooter,
}

View file

@ -0,0 +1,19 @@
import React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
(<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-white/10 bg-gray-950/40 px-3 py-2 text-sm text-white ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus:border-primary transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />)
);
})
Input.displayName = "Input"
export { Input }

View file

@ -0,0 +1,16 @@
import React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-300"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,120 @@
import React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-slate-800 bg-gray-900/50 px-3 py-2 text-sm ring-offset-background placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-slate-800 bg-gray-900 text-slate-50 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn("p-1", position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-slate-800 focus:text-slate-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span
className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100/10 dark:bg-slate-800", className)}
{...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View file

@ -0,0 +1,83 @@
import React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props} />
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props} />
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-slate-900 font-medium text-slate-50 dark:bg-slate-50 dark:text-slate-900", className)}
{...props} />
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-slate-100/50 data-[state=selected]:bg-slate-100 dark:hover:bg-slate-800/50 dark:data-[state=selected]:bg-slate-800",
className
)}
{...props} />
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-slate-500 [&:has([role=checkbox])]:pr-0 dark:text-slate-400",
className
)}
{...props} />
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props} />
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-slate-500 dark:text-slate-400", className)}
{...props} />
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View file

@ -0,0 +1,18 @@
import React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
(<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-white/10 bg-gray-950/40 px-3 py-2 text-sm text-white ring-offset-background placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus:border-primary transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />)
);
})
Textarea.displayName = "Textarea"
export { Textarea }

View file

@ -0,0 +1,104 @@
import { cn } from '@/lib/utils';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva } from 'class-variance-authority';
import { X } from 'lucide-react';
import React from 'react';
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full backdrop-blur-sm',
{
variants: {
variant: {
default: 'bg-gray-900/80 border-gray-700 text-gray-50',
destructive:
'group destructive border-red-500/50 bg-red-900/80 text-red-50',
success:
'group success border-green-500/50 bg-green-900/80 text-green-50',
},
},
defaultVariants: {
variant: 'default',
},
},
);
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-destructive/30 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
export {
Toast,
ToastAction,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
toastVariants,
};

View file

@ -0,0 +1,34 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from '@/components/ui/toast';
import { useToast } from '@/components/ui/use-toast';
import React from 'react';
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(({ id, title, description, action, ...props }) => {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View file

@ -0,0 +1,103 @@
import { useState, useEffect } from "react"
const TOAST_LIMIT = 1
let count = 0
function generateId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
const toastStore = {
state: {
toasts: [],
},
listeners: [],
getState: () => toastStore.state,
setState: (nextState) => {
if (typeof nextState === 'function') {
toastStore.state = nextState(toastStore.state)
} else {
toastStore.state = { ...toastStore.state, ...nextState }
}
toastStore.listeners.forEach(listener => listener(toastStore.state))
},
subscribe: (listener) => {
toastStore.listeners.push(listener)
return () => {
toastStore.listeners = toastStore.listeners.filter(l => l !== listener)
}
}
}
export const toast = ({ ...props }) => {
const id = generateId()
const update = (props) =>
toastStore.setState((state) => ({
...state,
toasts: state.toasts.map((t) =>
t.id === id ? { ...t, ...props } : t
),
}))
const dismiss = () => toastStore.setState((state) => ({
...state,
toasts: state.toasts.filter((t) => t.id !== id),
}))
toastStore.setState((state) => ({
...state,
toasts: [
{ ...props, id, dismiss },
...state.toasts,
].slice(0, TOAST_LIMIT),
}))
return {
id,
dismiss,
update,
}
}
export function useToast() {
const [state, setState] = useState(toastStore.getState())
useEffect(() => {
const unsubscribe = toastStore.subscribe((state) => {
setState(state)
})
return unsubscribe
}, [])
useEffect(() => {
const timeouts = []
state.toasts.forEach((toast) => {
if (toast.duration === Infinity) {
return
}
const timeout = setTimeout(() => {
toast.dismiss()
}, toast.duration || 5000)
timeouts.push(timeout)
})
return () => {
timeouts.forEach((timeout) => clearTimeout(timeout))
}
}, [state.toasts])
return {
toast,
toasts: state.toasts,
}
}

View file

@ -0,0 +1,105 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from './SupabaseAuthContext';
const NotificationContext = createContext();
export const NotificationProvider = ({ children }) => {
const { user } = useAuth();
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(true);
const fetchNotifications = useCallback(async () => {
if (!user) {
setLoading(false);
return;
}
setLoading(true);
const { data, error } = await supabase
.from('notifications')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching notifications:', error);
} else {
setNotifications(data);
setUnreadCount(data.filter(n => !n.is_read).length);
}
setLoading(false);
}, [user]);
useEffect(() => {
fetchNotifications();
}, [fetchNotifications]);
useEffect(() => {
if (!user) return;
const channel = supabase
.channel(`user_notifications:${user.id}`)
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications', filter: `user_id=eq.${user.id}` },
(payload) => {
setNotifications(prev => [payload.new, ...prev]);
setUnreadCount(prev => prev + 1);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user]);
const markAsRead = async (notificationId) => {
const { data, error } = await supabase
.from('notifications')
.update({ is_read: true })
.eq('id', notificationId)
.select()
.single();
if (error) {
console.error('Error marking notification as read:', error);
} else {
setNotifications(prev => prev.map(n => n.id === notificationId ? data : n));
setUnreadCount(prev => (prev > 0 ? prev - 1 : 0));
}
};
const markAllAsRead = async () => {
if (!user) return;
const { error } = await supabase
.from('notifications')
.update({ is_read: true })
.eq('user_id', user.id)
.eq('is_read', false);
if (error) {
console.error('Error marking all notifications as read:', error);
} else {
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
setUnreadCount(0);
}
};
const value = {
notifications,
unreadCount,
loading,
markAsRead,
markAllAsRead,
};
return <NotificationContext.Provider value={value}>{children}</NotificationContext.Provider>;
};
export const useNotifications = () => {
const context = useContext(NotificationContext);
if (context === undefined) {
throw new Error('useNotifications must be used within a NotificationProvider');
}
return context;
};

View file

@ -0,0 +1,92 @@
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { useAuth } from '@/contexts/SupabaseAuthContext';
const SiteContext = createContext(undefined);
const SITE_NAME = "AeThex Corporate";
export const SiteProvider = ({ children }) => {
const { toast } = useToast();
const { user, profile, loading: authLoading } = useAuth();
const [siteId, setSiteId] = useState(null);
const [siteConfig, setSiteConfig] = useState(null);
const [loading, setLoading] = useState(true);
const initializeSite = useCallback(async (currentUser, currentUserProfile) => {
if (!currentUser || !currentUserProfile) {
setSiteId(null);
setSiteConfig(null);
setLoading(false);
return;
}
setLoading(true);
try {
const { data, error } = await supabase.rpc('get_or_create_site_config', {
p_site_name: SITE_NAME
});
if (error) {
throw error;
}
if (data && data.length > 0) {
const config = data[0];
setSiteConfig(config);
setSiteId(config.site_id);
} else {
// This case should ideally not be reached due to the function's design,
// but it's good practice to handle it.
toast({
variant: "warning",
title: "Site Not Found",
description: "Could not retrieve or create site configuration.",
});
setSiteId(null);
setSiteConfig(null);
}
} catch (error) {
console.error("Error initializing site:", error);
toast({
variant: "destructive",
title: "Site Initialization Failed",
description: error.message || "Could not initialize the site.",
});
setSiteId(null);
setSiteConfig(null);
} finally {
setLoading(false);
}
}, [toast]);
useEffect(() => {
if (!authLoading) {
initializeSite(user, profile);
}
}, [user, profile, authLoading, initializeSite]);
const refreshSiteConfig = useCallback(async () => {
if (user && profile) {
await initializeSite(user, profile);
}
}, [user, profile, initializeSite]);
const value = useMemo(() => ({
siteId,
siteConfig,
loading: authLoading || loading,
refreshSiteConfig,
}), [siteId, siteConfig, authLoading, loading, refreshSiteConfig]);
return <SiteContext.Provider value={value}>{children}</SiteContext.Provider>;
};
export const useSite = () => {
const context = useContext(SiteContext);
if (context === undefined) {
throw new Error('useSite must be used within a SiteProvider');
}
return context;
};

View file

@ -0,0 +1,195 @@
import React, { createContext, useState, useEffect, useContext, useCallback, useMemo } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
const SupabaseAuthContext = createContext(null);
export const useAuth = () => {
const context = useContext(SupabaseAuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [profile, setProfile] = useState(null);
const [session, setSession] = useState(null);
const [loading, setLoading] = useState(true);
const [showAuthModal, setShowAuthModal] = useState(false);
const { toast } = useToast();
const fetchProfile = useCallback(async (userId) => {
if (!userId) return null;
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single();
if (error && error.code !== 'PGRST116') {
console.error('Error fetching profile:', error.message);
return null;
};
return data || null;
} catch (error) {
console.error('Error fetching profile:', error.message);
return null;
}
}, []);
const handleAuthStateChange = useCallback(async (event, session) => {
setLoading(true);
setSession(session);
const currentUser = session?.user ?? null;
setUser(currentUser);
if (currentUser) {
const userProfile = await fetchProfile(currentUser.id);
setProfile(userProfile);
} else {
setProfile(null);
}
if (event === 'SIGNED_IN') {
setShowAuthModal(false);
toast({
title: "Signed In Successfully",
description: `Welcome back, ${session?.user.email}!`,
});
}
if (event === 'SIGNED_OUT') {
setProfile(null);
setUser(null);
setSession(null);
// No toast on programmatic sign-out to avoid duplicate messages.
}
if (event === 'TOKEN_REFRESHED' && session === null) {
// This case handles the "Invalid Refresh Token" error implicitly.
// Supabase sets session to null after a failed refresh.
signOut(true); // Pass true to show a toast
}
setLoading(false);
}, [fetchProfile, toast]);
useEffect(() => {
setLoading(true);
supabase.auth.getSession().then(({ data: { session }, error }) => {
if (error && error.message.includes('Invalid Refresh Token')) {
signOut(true);
} else {
handleAuthStateChange('INITIAL_SESSION', session);
}
}).finally(() => setLoading(false));
const { data: authListener } = supabase.auth.onAuthStateChange(handleAuthStateChange);
return () => {
authListener.subscription.unsubscribe();
};
}, [handleAuthStateChange]);
const signIn = async (email, password) => {
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
toast({
variant: "destructive",
title: "Sign In Failed",
description: error.message,
});
}
return { error };
};
const signUp = async (email, password) => {
const { error } = await supabase.auth.signUp({ email, password });
if (error) {
toast({
variant: "destructive",
title: "Sign Up Failed",
description: error.message,
});
}
return { error };
};
const signOut = async (showSessionExpiredToast = false) => {
const { error } = await supabase.auth.signOut();
if (error) {
toast({
variant: "destructive",
title: "Sign Out Failed",
description: error.message,
});
} else {
if (showSessionExpiredToast) {
toast({
variant: "destructive",
title: "Session Expired",
description: "Your session has expired. Please sign in again.",
});
} else {
toast({
title: "Signed Out",
description: "You have been successfully signed out.",
});
}
}
setProfile(null);
setUser(null);
setSession(null);
return { error };
};
const sendPasswordResetEmail = async (email) => {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/password-reset`,
});
if (error) {
toast({
variant: "destructive",
title: "Error Sending Email",
description: error.message,
});
}
return { error };
};
const signInWithProvider = async (provider) => {
const { error } = await supabase.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo: window.location.origin,
},
});
if (error) {
toast({
variant: "destructive",
title: `${provider} Sign In Failed`,
description: error.message,
});
}
return { error };
};
const value = useMemo(() => ({
user,
profile,
session,
loading,
showAuthModal,
setShowAuthModal,
signIn,
signUp,
signOut,
signInWithGitHub: () => signInWithProvider('github'),
signInWithGoogle: () => signInWithProvider('google'),
sendPasswordResetEmail,
}), [user, profile, session, loading, showAuthModal]);
return (
<SupabaseAuthContext.Provider value={value}>
{children}
</SupabaseAuthContext.Provider>
);
};

View file

@ -0,0 +1,172 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { useToast } from '@/components/ui/use-toast';
const CATEGORIES = {
'tech-summit': { label: 'Tech Summit', color: 'bg-blue-500' },
'dev-workshop': { label: 'Dev Workshop', color: 'bg-green-500' },
'product-launch': { label: 'Product Launch', color: 'bg-purple-500' },
'networking-mixer': { label: 'Networking Mixer', color: 'bg-yellow-500' },
};
export const useEvents = () => {
const { user } = useAuth();
const { toast } = useToast();
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [registrationLoading, setRegistrationLoading] = useState(false);
const [registeredEvents, setRegisteredEvents] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const fetchEvents = useCallback(async () => {
setLoading(true);
const { data, error } = await supabase
.from('aethex_events')
.select('*, aethex_event_registrations(count)');
if (error) {
console.error('Error fetching events:', error);
toast({
variant: "destructive",
title: "Error",
description: "Could not fetch events.",
});
setEvents([]);
} else {
const formattedEvents = data.map(event => ({
...event,
registered_count: event.aethex_event_registrations[0]?.count || 0,
}));
setEvents(formattedEvents);
}
setLoading(false);
}, [toast]);
const fetchRegisteredEvents = useCallback(async () => {
if (!user) {
setRegisteredEvents([]);
return;
}
const { data, error } = await supabase
.from('aethex_event_registrations')
.select('event_id')
.eq('user_id', user.id);
if (error) {
console.error('Error fetching registered events:', error);
} else {
setRegisteredEvents(data.map(reg => reg.event_id));
}
}, [user]);
useEffect(() => {
fetchEvents();
}, [fetchEvents]);
useEffect(() => {
fetchRegisteredEvents();
}, [user, fetchRegisteredEvents]);
const handleRegister = useCallback(async (eventId) => {
if (!user) {
toast({
variant: "destructive",
title: "Authentication Required",
description: "You must be signed in to register for an event.",
});
return;
}
setRegistrationLoading(true);
const { error } = await supabase
.from('aethex_event_registrations')
.insert({ event_id: eventId, user_id: user.id });
if (error) {
console.error('Error registering for event:', error);
toast({
variant: "destructive",
title: "Registration Failed",
description: error.message,
});
} else {
toast({
variant: "success",
title: "Registration Successful!",
description: "You've successfully registered for the event and earned 100 loyalty points!",
});
setRegisteredEvents(prev => [...prev, eventId]);
setEvents(prevEvents => prevEvents.map(event =>
event.id === eventId ? { ...event, registered_count: event.registered_count + 1 } : event
));
}
setRegistrationLoading(false);
}, [user, toast]);
const handleUnregister = useCallback(async (eventId) => {
if (!user) return;
setRegistrationLoading(true);
const { error } = await supabase
.from('aethex_event_registrations')
.delete()
.match({ event_id: eventId, user_id: user.id });
if (error) {
console.error('Error unregistering from event:', error);
toast({
variant: "destructive",
title: "Unregistration Failed",
description: error.message,
});
} else {
toast({
title: "Unregistered Successfully",
description: "You have been unregistered from the event.",
});
setRegisteredEvents(prev => prev.filter(id => id !== eventId));
setEvents(prevEvents => prevEvents.map(event =>
event.id === eventId ? { ...event, registered_count: Math.max(0, event.registered_count - 1) } : event
));
}
setRegistrationLoading(false);
}, [user, toast]);
const filteredEvents = useMemo(() => {
return events
.filter(event =>
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
event.description.toLowerCase().includes(searchTerm.toLowerCase())
)
.filter(event =>
selectedCategory === 'all' || event.category === selectedCategory
);
}, [events, searchTerm, selectedCategory]);
const getCategoryColor = useCallback((category) => {
return CATEGORIES[category]?.color || 'bg-gray-500';
}, []);
const getCategoryLabel = useCallback((category) => {
return CATEGORIES[category]?.label || 'General';
}, []);
const categories = useMemo(() => ['all', ...Object.keys(CATEGORIES)], []);
return {
events,
filteredEvents,
loading,
registrationLoading,
registeredEvents,
searchTerm,
setSearchTerm,
selectedCategory,
setSelectedCategory,
handleRegister,
handleUnregister,
getCategoryColor,
getCategoryLabel,
categories,
};
};

View file

@ -0,0 +1,18 @@
import { useState, useCallback } from 'react';
export const useForm = (initialState = {}) => {
const [values, setValues] = useState(initialState);
const reset = useCallback(() => {
setValues(initialState);
}, [initialState]);
const handleInputChange = useCallback(({ target }) => {
setValues(prev => ({
...prev,
[target.name]: target.type === 'checkbox' ? target.checked : target.value,
}));
}, []);
return { values, setValues, handleInputChange, reset };
};

120
contribute/src/index.css Normal file
View file

@ -0,0 +1,120 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap');
body {
font-family: 'Courier Prime', 'Courier New', Courier, monospace;
background-color: #03040B;
color: hsl(var(--foreground));
min-height: 100vh;
}
:root {
--background: 228 33% 4%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 210 100% 56%; /* Aethex Blue */
--secondary: 260 100% 70%; /* Aethex Purple */
--primary-foreground: 210 20% 98%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 210 100% 56%;
--radius: 0.75rem;
}
* {
border-color: hsl(var(--border));
}
.font-mono {
font-family: 'Courier Prime', 'Courier New', Courier, monospace;
}
.radial-gradient-background {
background-color: #0a0e14;
background-image: radial-gradient(circle at 1px 1px, hsl(var(--border)) 1px, transparent 0);
background-size: 40px 40px;
}
.aurora-background {
position: relative;
background-color: #03040B;
overflow: hidden;
}
.aurora-background::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 150%;
height: 150%;
background-image:
radial-gradient(ellipse at 20% 30%, hsl(var(--primary) / 0.1), transparent 60%),
radial-gradient(ellipse at 80% 60%, hsl(var(--secondary) / 0.1), transparent 60%);
transform: translate(-50%, -50%);
animation: aurora-flow 20s linear infinite;
z-index: 0;
}
@keyframes aurora-flow {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: hsl(var(--background));
}
::-webkit-scrollbar-thumb {
background-color: hsl(var(--primary));
border-radius: 20px;
border: 3px solid hsl(var(--background));
}
::-webkit-scrollbar-thumb:hover {
background-color: hsl(204, 80%, 63%);
}
.prose {
--tw-prose-body: theme(colors.gray.300);
--tw-prose-headings: theme(colors.white);
--tw-prose-lead: theme(colors.gray.400);
--tw-prose-links: hsl(var(--primary)); /* Use direct HSL from --primary */
--tw-prose-bold: theme(colors.white);
--tw-prose-counters: theme(colors.gray.400);
--tw-prose-bullets: theme(colors.gray.600);
--tw-prose-hr: theme(colors.gray.700);
--tw-prose-quotes: theme(colors.gray.200);
--tw-prose-quote-borders: hsl(var(--primary)); /* Use direct HSL from --primary */
--tw-prose-captions: theme(colors.gray.400);
--tw-prose-code: hsl(var(--primary)); /* Use direct HSL from --primary */
--tw-prose-pre-code: theme(colors.gray.300);
--tw-prose-pre-bg: theme(colors.gray.900);
--tw-prose-th-borders: theme(colors.gray.600);
--tw-prose-td-borders: theme(colors.gray.700);
}

View file

@ -0,0 +1,13 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://kmdeisowhtsalsekkzqd.supabase.co';
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImttZGVpc293aHRzYWxzZWtrenFkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTM3Mzc2NTIsImV4cCI6MjA2OTMxMzY1Mn0.2mvk-rDZnHOzdx6Cgcysh51a3cflOlRWO6OA1Z5YWuQ';
const customSupabaseClient = createClient(supabaseUrl, supabaseAnonKey);
export default customSupabaseClient;
export {
customSupabaseClient,
customSupabaseClient as supabase,
};

View file

@ -0,0 +1,61 @@
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
export const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
return adjustedDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
export const formatTime = (timeString) => {
if (!timeString) return 'N/A';
const [hours, minutes] = timeString.split(':');
const date = new Date();
date.setHours(hours);
date.setMinutes(minutes);
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
};
export const timeAgo = (dateString) => {
if (!dateString) return null;
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years ago";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months ago";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days ago";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours ago";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes ago";
}
return Math.floor(seconds) + " seconds ago";
}

28
contribute/src/main.jsx Normal file
View file

@ -0,0 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from '@/App';
import '@/index.css';
import { AuthProvider } from '@/contexts/SupabaseAuthContext';
import { SiteProvider } from '@/contexts/SiteContext';
import { NotificationProvider } from '@/contexts/NotificationContext';
const AppWrapper = () => {
return (
<BrowserRouter>
<AuthProvider>
<NotificationProvider>
<SiteProvider>
<App />
</SiteProvider>
</NotificationProvider>
</AuthProvider>
</BrowserRouter>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AppWrapper />
</React.StrictMode>
);

View file

@ -0,0 +1,119 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
const AboutPage = () => {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3,
}
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } }
};
return (
<>
<Helmet>
<title>About AeThex | Our Mission, Vision, and Team</title>
<meta name="description" content="Learn about AeThex's mission to build the future of digital infrastructure, our core values, and the team of visionaries driving us forward." />
<meta property="og:title" content="About AeThex | Our Mission, Vision, and Team" />
<meta property="og:description" content="Learn about AeThex's mission, vision, and the team behind our innovations." />
</Helmet>
<div className="aurora-background -mx-4 -mt-8">
<div className="relative z-10 container mx-auto text-white py-16 sm:py-24 px-4">
<motion.div
className="text-center"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7 }}
>
<h1 className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono">
About <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">AeThex</span>
</h1>
<p className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
We are a collective of architects, innovators, and visionaries dedicated to engineering the foundational systems for the next digital epoch.
</p>
</motion.div>
</div>
</div>
<div className="py-16 sm:py-24">
<motion.div
className="grid md:grid-cols-2 gap-12 items-center mb-24"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
>
<motion.div variants={itemVariants}>
<img className="rounded-xl shadow-2xl shadow-primary/10 w-full h-auto" alt="A diverse team of engineers collaborating around a futuristic holographic display" src="https://images.unsplash.com/photo-1531497258014-b5736f376b1b" />
</motion.div>
<motion.div variants={itemVariants} className="prose prose-lg prose-invert max-w-none">
<h2 className="font-mono text-3xl">Our Story</h2>
<p>AeThex was born from a simple yet profound observation: the digital world was built on centralized, fragile foundations. We envisioned a new internetmore resilient, intelligent, and equitable. We are not just a company; we are a movement to build that future.</p>
<p>Our journey began with a small group of experts in decentralized systems, AI, and cryptography. Today, we are a growing ecosystem of contributors, each an owner, all unified by a single, audacious goal.</p>
</motion.div>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
>
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 font-mono">Our Core Principles</h2>
<motion.div variants={itemVariants} className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
<Card className="bg-gray-900/40 border-white/10">
<CardHeader>
<CardTitle>Decentralization First</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400">We prioritize architectural designs that distribute power, eliminate single points of failure, and resist censorship.</p>
</CardContent>
</Card>
<Card className="bg-gray-900/40 border-white/10">
<CardHeader>
<CardTitle>Radical Ownership</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400">Our contributors are co-owners. We believe that those who build the future should have a stake in it.</p>
</CardContent>
</Card>
<Card className="bg-gray-900/40 border-white/10">
<CardHeader>
<CardTitle>Long-Term Vision</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400">We are not driven by short-term trends. Our focus is on building robust, scalable, and enduring infrastructure.</p>
</CardContent>
</Card>
<Card className="bg-gray-900/40 border-white/10">
<CardHeader>
<CardTitle>Open Collaboration</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400">We foster an environment of intense, transparent collaboration, believing that the best ideas emerge from diverse perspectives.</p>
</CardContent>
</Card>
</motion.div>
</motion.div>
</div>
</>
);
};
export default AboutPage;

View file

@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Loader2, Mail, Phone, MapPin } from 'lucide-react';
const ContactPage = () => {
const [formData, setFormData] = useState({ name: '', email: '', subject: '', message: '' });
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const handleChange = (e) => {
setFormData({ ...formData, [e.target.id]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await supabase.from('contact_submissions').insert([
{ name: formData.name, email: formData.email, subject: formData.subject, message: formData.message }
]);
if (error) {
toast({
variant: 'destructive',
title: 'Error submitting form',
description: error.message,
});
} else {
toast({
title: 'Message Sent!',
description: "Thank you for reaching out. We'll be in touch soon.",
});
setFormData({ name: '', email: '', subject: '', message: '' });
}
setLoading(false);
};
const contactInfo = [
{ icon: Mail, title: "General Inquiries", value: "contact@aethex.com" },
{ icon: Phone, title: "Phone Support", value: "+1 (555) 010-4567" },
{ icon: MapPin, title: "Headquarters", value: "Digital Realm / Decentralized" },
];
return (
<>
<Helmet>
<title>Contact Us | AeThex</title>
<meta name="description" content="Get in touch with the AeThex team for inquiries, partnerships, or press via our contact form or official channels." />
<meta property="og:title" content="Contact Us | AeThex" />
<meta property="og:description" content="Reach out to the AeThex collective." />
</Helmet>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="max-w-6xl mx-auto"
>
<div className="text-center mb-16">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono"
>
Contact <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Us</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto"
>
Have a question, proposal, or just want to connect? We're here to listen.
</motion.p>
</div>
<div className="grid md:grid-cols-5 gap-12">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
className="md:col-span-2 space-y-8"
>
{contactInfo.map((item, index) => (
<div key={index} className="flex gap-4 items-start">
<div className="p-3 bg-primary/10 rounded-lg w-max border border-primary/20">
<item.icon className="w-6 h-6 text-primary" />
</div>
<div>
<h3 className="font-bold text-white text-lg">{item.title}</h3>
<p className="text-gray-400">{item.value}</p>
</div>
</div>
))}
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="md:col-span-3"
>
<Card className="bg-gray-900/40 border-white/10">
<CardHeader>
<CardTitle>Send us a message</CardTitle>
<CardDescription>Fill out the form below and we'll get back to you.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input id="name" type="text" value={formData.name} onChange={handleChange} required disabled={loading} placeholder="John Doe" />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input id="email" type="email" value={formData.email} onChange={handleChange} required disabled={loading} placeholder="you@example.com" />
</div>
<div className="space-y-2">
<Label htmlFor="subject">Subject</Label>
<Input id="subject" type="text" value={formData.subject} onChange={handleChange} disabled={loading} placeholder="Partnership Inquiry" />
</div>
<div className="space-y-2">
<Label htmlFor="message">Your Message</Label>
<Textarea id="message" value={formData.message} onChange={handleChange} required disabled={loading} rows={5} placeholder="Tell us how we can help..." />
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Send Message
</Button>
</form>
</CardContent>
</Card>
</motion.div>
</div>
</motion.div>
</>
);
};
export default ContactPage;

View file

@ -0,0 +1,210 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Loader2, ArrowRight, CheckCircle } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.15 }
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } }
};
const GetInvolvedPage = () => {
const [opportunities, setOpportunities] = useState([]);
const [testimonials, setTestimonials] = useState([]);
const [faqs, setFaqs] = useState([]);
const [loading, setLoading] = useState(true);
const [applying, setApplying] = useState(null);
const [applied, setApplied] = useState([]);
const { user, profile, setShowAuthModal } = useAuth();
const { toast } = useToast();
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const [opps, tests, faqsData, apps] = await Promise.all([
supabase.from('volunteer_opportunities').select('*').eq('is_active', true),
supabase.from('volunteer_testimonials').select('*'),
supabase.from('faqs').select('*').eq('category', 'Volunteering').order('display_order'),
user ? supabase.from('volunteer_applications').select('opportunity_id').eq('user_id', user.id) : Promise.resolve({ data: [] })
]);
if (opps.error) throw opps.error;
if (tests.error) throw tests.error;
if (faqsData.error) throw faqsData.error;
if (apps.error) throw apps.error;
setOpportunities(opps.data);
setTestimonials(tests.data);
setFaqs(faqsData.data);
setApplied(apps.data.map(a => a.opportunity_id));
} catch (error) {
toast({ variant: 'destructive', title: 'Error fetching data', description: error.message });
} finally {
setLoading(false);
}
};
fetchData();
}, [toast, user]);
const handleApply = async (opportunityId) => {
if (!user) {
setShowAuthModal(true);
toast({ title: 'Please sign in', description: 'You need to be logged in to apply for an opportunity.' });
return;
}
setApplying(opportunityId);
const { data, error } = await supabase
.from('volunteer_applications')
.insert({ opportunity_id: opportunityId, user_id: user.id });
if (error) {
toast({
variant: 'destructive',
title: 'Application failed',
description: error.message,
});
} else {
toast({
variant: 'success',
title: 'Application Submitted!',
description: 'Thank you for your interest. We will review your application.',
});
setApplied(prev => [...prev, opportunityId]);
}
setApplying(null);
};
return (
<>
<Helmet>
<title>Get Involved | AeThex Collective</title>
<meta name="description" content="Join the AeThex collective. Find volunteer opportunities, learn about our mission, and become a co-owner of the future of the internet." />
<meta property="og:title" content="Get Involved | AeThex Collective" />
<meta property="og:description" content="Contribute your skills to build decentralized, intelligent technologies." />
</Helmet>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="max-w-6xl mx-auto"
>
<motion.div variants={itemVariants} className="text-center mb-16">
<h1 className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono">
Join the <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Collective</span>
</h1>
<p className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
We are building a decentralized future, and we believe it should be built by a decentralized community. Your skills, passion, and vision can help shape the next digital epoch.
</p>
</motion.div>
<motion.section variants={itemVariants} className="mb-24">
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 font-mono">Available Opportunities</h2>
{loading ? (
<div className="flex justify-center"><Loader2 className="h-8 w-8 animate-spin" /></div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{opportunities.map(opp => (
<Card key={opp.id} className="flex flex-col">
<CardHeader>
<CardTitle>{opp.title}</CardTitle>
<CardDescription>{opp.time_commitment}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-gray-400 mb-4">{opp.description}</p>
<div className="space-y-1">
<h4 className="font-bold text-sm text-white">Skills Required:</h4>
<div className="flex flex-wrap gap-2">
{opp.skills_required.map(skill => <Badge key={skill} variant="secondary">{skill}</Badge>)}
</div>
</div>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => handleApply(opp.id)}
disabled={applying === opp.id || applied.includes(opp.id)}
>
{applying === opp.id ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : applied.includes(opp.id) ? (
<>
<CheckCircle className="mr-2 h-4 w-4" /> Applied
</>
) : (
<>
Apply Now <ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</CardFooter>
</Card>
))}
</div>
)}
</motion.section>
<motion.section variants={itemVariants} className="mb-24 bg-gray-900/30 rounded-xl p-8 md:p-12 border border-white/10">
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 font-mono">Voices of the Collective</h2>
{loading ? (
<div className="flex justify-center"><Loader2 className="h-8 w-8 animate-spin" /></div>
) : (
<div className="grid md:grid-cols-2 gap-8">
{testimonials.map(t => (
<blockquote key={t.id} className="p-6 border-l-4 border-primary bg-gray-950/40 rounded-r-lg">
<p className="text-gray-300 italic">"{t.testimonial}"</p>
<footer className="mt-4 flex items-center gap-3">
<img class="h-12 w-12 rounded-full object-cover" alt={`Avatar of ${t.name}`} src="https://images.unsplash.com/photo-1701269395744-32e3c1d11645" />
<div>
<p className="font-bold text-white">{t.name}</p>
<p className="text-sm text-primary">{t.role}</p>
</div>
</footer>
</blockquote>
))}
</div>
)}
</motion.section>
<motion.section variants={itemVariants} className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 font-mono">Frequently Asked Questions</h2>
{loading ? (
<div className="flex justify-center"><Loader2 className="h-8 w-8 animate-spin" /></div>
) : (
<Accordion type="single" collapsible className="w-full">
{faqs.map(faq => (
<AccordionItem key={faq.id} value={`item-${faq.id}`}>
<AccordionTrigger className="text-lg text-left">{faq.question}</AccordionTrigger>
<AccordionContent className="text-base text-gray-400">
{faq.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</motion.section>
</motion.div>
</>
);
};
export default GetInvolvedPage;

View file

@ -0,0 +1,154 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowRight, Cpu, Shield, Users, Sparkles } from 'lucide-react';
const HomePage = () => {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } },
};
const services = [
{
icon: Cpu,
title: "Decentralized AI",
description: "Pioneering intelligent systems that operate on distributed networks for unparalleled security and autonomy.",
link: "/technology#ai"
},
{
icon: Shield,
title: "Next-Gen Protocols",
description: "Architecting the foundational communication and data protocols for a truly decentralized internet.",
link: "/technology#protocols"
},
{
icon: Users,
title: "Digital Sovereignty",
description: "Building tools and platforms that empower individuals with true ownership of their data and digital identity.",
link: "/technology#sovereignty"
},
];
return (
<>
<Helmet>
<title>AeThex | Engineering the Next Digital Epoch</title>
<meta name="description" content="AeThex is a research and engineering collective building the foundational layers for the next generation of the internet. Discover our work in decentralized AI, next-gen protocols, and digital sovereignty." />
<meta property="og:title" content="AeThex | Engineering the Next Digital Epoch" />
<meta property="og:description" content="Pioneering the future of decentralized systems, AI, and digital ownership." />
</Helmet>
<div className="relative -mx-4 -mt-8 mb-16 overflow-hidden">
<div className="aurora-background">
<div className="relative z-10 container mx-auto text-white py-24 sm:py-32 px-4 text-center">
<motion.h1
className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7 }}
>
Engineering the Next <br className="hidden md:block"/>Digital <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Epoch</span>
</motion.h1>
<motion.p
className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2 }}
>
AeThex is a research and engineering collective building the foundational layers for the next generation of the internet. We create decentralized, intelligent, and user-centric technologies to foster a more resilient and equitable digital world.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.4 }}
className="flex justify-center items-center gap-4"
>
<Button asChild size="lg">
<Link to="/get-involved">
Get Involved <Sparkles className="ml-2 h-5 w-5" />
</Link>
</Button>
<Button asChild size="lg" variant="outline">
<Link to="/about">
Our Mission <ArrowRight className="ml-2 h-5 w-5" />
</Link>
</Button>
</motion.div>
</div>
</div>
</div>
<div className="py-16 sm:py-24">
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
>
<motion.h2 variants={itemVariants} className="text-3xl md:text-4xl font-bold text-center mb-4 font-mono tracking-tight">Our Focus Areas</motion.h2>
<motion.p variants={itemVariants} className="text-lg text-gray-400 text-center max-w-2xl mx-auto mb-16">
We are deeply engaged in solving the most complex challenges at the intersection of decentralization and artificial intelligence.
</motion.p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{services.map((service, index) => (
<motion.div key={index} variants={itemVariants}>
<Card className="h-full bg-gray-900/40 border-white/10 hover:border-primary/50 hover:bg-primary/10 transition-all duration-300">
<CardHeader>
<div className="p-3 bg-primary/10 rounded-lg w-max mb-4 border border-primary/20">
<service.icon className="w-8 h-8 text-primary" />
</div>
<CardTitle>{service.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400">{service.description}</p>
<Link to={service.link} className="text-primary font-semibold mt-4 inline-block hover:underline">
Learn More &rarr;
</Link>
</CardContent>
</Card>
</motion.div>
))}
</div>
</motion.div>
</div>
<div className="py-16 sm:py-24">
<motion.div
className="grid md:grid-cols-2 gap-12 items-center"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
>
<motion.div variants={itemVariants}>
<h2 className="font-mono text-3xl font-bold tracking-tight">For the Builders, by the Builders</h2>
<p className="mt-4 text-lg text-gray-400">AeThex operates as an equity-based collective. We are not hiring employees; we are onboarding partners and co-owners who share our long-term vision. Our structure is designed for those who seek not just to participate in the future, but to build and own a piece of it.</p>
<Button asChild size="lg" className="mt-6">
<Link to="/get-involved">Join the Collective</Link>
</Button>
</motion.div>
<motion.div variants={itemVariants}>
<img class="rounded-xl shadow-2xl shadow-primary/10 w-full h-auto" alt="A group of diverse software engineers collaborating intently in a modern, dark-themed office space" src="https://images.unsplash.com/photo-1651009188116-bb5f80eaf6aa" />
</motion.div>
</motion.div>
</div>
</>
);
};
export default HomePage;

View file

@ -0,0 +1,245 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useDropzone } from 'react-dropzone';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { useToast } from '@/components/ui/use-toast';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Loader2, ArrowLeft, Briefcase, MapPin, UploadCloud, FileText, Trash2, ShieldCheck } from 'lucide-react';
const JobApplicationPage = () => {
const { id } = useParams();
const navigate = useNavigate();
const { user } = useAuth();
const { toast } = useToast();
const [job, setJob] = useState(null);
const [loading, setLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [coverLetter, setCoverLetter] = useState('');
const [resumeFile, setResumeFile] = useState(null);
useEffect(() => {
const fetchJob = async () => {
setLoading(true);
const { data, error } = await supabase
.from('job_openings')
.select('*')
.eq('id', id)
.single();
if (error || !data) {
toast({ variant: 'destructive', title: 'Error', description: 'Could not find the job opening.' });
navigate('/');
return;
}
setJob(data);
const { data: application, error: appError } = await supabase
.from('job_applications')
.select('id')
.eq('user_id', user.id)
.eq('job_id', id)
.maybeSingle();
if (application) {
toast({ title: 'Already Applied', description: "You have already applied for this position." });
navigate(`/job/${id}`);
}
setLoading(false);
};
if (user) {
fetchJob();
}
}, [id, user, navigate, toast]);
const onDrop = (acceptedFiles) => {
if (acceptedFiles.length > 0) {
const file = acceptedFiles[0];
if(file.size > 5 * 1024 * 1024) {
toast({
variant: "destructive",
title: "File too large",
description: "Resume file must be under 5MB.",
});
return;
}
setResumeFile(file);
}
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { 'application/pdf': ['.pdf'] },
maxFiles: 1,
});
const handleSubmit = async (e) => {
e.preventDefault();
if (!resumeFile) {
toast({ variant: 'destructive', title: 'Resume Required', description: 'Please upload your resume to apply.' });
return;
}
setIsSubmitting(true);
let resumeUrl = null;
try {
const fileExt = resumeFile.name.split('.').pop();
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
const filePath = `resumes/${fileName}`;
const { error: uploadError } = await supabase.storage
.from('job-applications')
.upload(filePath, resumeFile);
if (uploadError) throw uploadError;
const { data: urlData } = supabase.storage
.from('job-applications')
.getPublicUrl(filePath);
resumeUrl = urlData.publicUrl;
const { error: insertError } = await supabase
.from('job_applications')
.insert({
job_id: job.id,
user_id: user.id,
status: 'submitted',
cover_letter: coverLetter,
resume_url: resumeUrl,
submitted_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
if (insertError) throw insertError;
toast({
variant: "success",
title: "Application Submitted!",
description: `Your application for ${job.title} has been received.`,
});
navigate('/my-applications');
} catch (error) {
toast({
variant: "destructive",
title: "Application Failed",
description: error.message,
});
} finally {
setIsSubmitting(false);
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-[60vh]">
<Loader2 className="w-12 h-12 text-primary animate-spin" />
</div>
);
}
if (!job) return null;
return (
<>
<Helmet>
<title>{`Apply for ${job.title} - AeThex Careers`}</title>
<meta name="description" content={`Submit your application for the ${job.title} role at AeThex.`} />
</Helmet>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="mb-8">
<Button asChild variant="ghost" className="mb-4">
<Link to={`/job/${id}`} className="flex items-center text-gray-400 hover:text-white">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Job Details
</Link>
</Button>
</div>
<div className="max-w-4xl mx-auto">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-3xl font-bold">Apply for {job.title}</CardTitle>
<CardDescription className="flex justify-center items-center gap-4 pt-2">
<span className="flex items-center gap-1.5"><Briefcase className="w-4 h-4 text-primary" /> {job.division} / {job.department}</span>
<span className="flex items-center gap-1.5"><MapPin className="w-4 h-4 text-primary" /> {job.location}</span>
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-8">
<div className="space-y-2">
<Label htmlFor="coverLetter">Cover Letter (Optional)</Label>
<Textarea
id="coverLetter"
placeholder="Tell us why you're a great fit for this equity-track role..."
value={coverLetter}
onChange={(e) => setCoverLetter(e.target.value)}
rows={8}
className="bg-gray-900/50"
/>
</div>
<div className="space-y-2">
<Label htmlFor="resume">Resume (PDF, max 5MB)</Label>
<div
{...getRootProps()}
className={`flex justify-center items-center w-full px-6 py-10 border-2 border-dashed rounded-lg cursor-pointer transition-colors ${isDragActive ? 'border-primary bg-primary/10' : 'border-gray-700 hover:border-gray-600'}`}
>
<input {...getInputProps()} />
<div className="text-center">
<UploadCloud className="w-12 h-12 mx-auto text-gray-500 mb-3" />
<p className="font-semibold text-white">
{isDragActive ? "Drop the file here..." : "Drag & drop your resume here, or click to select"}
</p>
<p className="text-xs text-gray-500">PDF format only, up to 5MB</p>
</div>
</div>
{resumeFile && (
<div className="mt-4 flex items-center justify-between p-3 bg-gray-800 rounded-md">
<div className="flex items-center gap-3">
<FileText className="w-6 h-6 text-primary" />
<span className="text-sm text-white">{resumeFile.name}</span>
</div>
<Button type="button" variant="destructive" size="icon" onClick={() => setResumeFile(null)}>
<Trash2 className="w-4 h-4" />
</Button>
</div>
)}
</div>
<div className="flex flex-col items-center space-y-4">
<div className="flex items-center space-x-2 text-sm text-green-400">
<ShieldCheck className="w-5 h-5"/>
<span>By submitting, you agree to our NDA and the terms of this equity-based role.</span>
</div>
<Button type="submit" size="lg" className="w-full max-w-xs" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Submitting Application
</>
) : "Confirm & Submit Application"}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</motion.div>
</>
);
};
export default JobApplicationPage;

View file

@ -0,0 +1,227 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { Loader2, Briefcase, MapPin, ArrowLeft, CheckCircle, AlertCircle, AlertTriangle, Lock, Flag } from 'lucide-react';
import { timeAgo } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
const EquityDisclaimer = () => (
<motion.div
className="my-8 p-6 bg-yellow-900/20 border-2 border-yellow-500/30 rounded-xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="flex items-start gap-4">
<AlertTriangle className="w-10 h-10 text-yellow-400 flex-shrink-0 mt-1" />
<div>
<h3 className="text-xl font-bold text-yellow-300 mb-2">READ BEFORE APPLYING THIS IS AN EQUITY ROLE</h3>
<p className="text-yellow-200/80">
This is not a job in the traditional sense. There is no guaranteed pay, salary, or benefits. Instead, this is an <strong>equity-track contributor role</strong> for individuals aligned with AeThexs long-term mission.
</p>
</div>
</div>
</motion.div>
);
const NdaDisclaimer = () => (
<motion.div
className="my-8 p-6 bg-red-900/20 border-2 border-red-500/30 rounded-xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<div className="flex items-start gap-4">
<Lock className="w-10 h-10 text-red-400 flex-shrink-0 mt-1" />
<div>
<h3 className="text-xl font-bold text-red-300 mb-2">Strict Confidentiality Required</h3>
<p className="text-red-200/80">
Due to the sensitive nature of our work, all contributors must sign a comprehensive Non-Disclosure Agreement (NDA). This protects our intellectual property and the future we are building together.
</p>
</div>
</div>
</motion.div>
);
const FinalNote = () => (
<motion.div
className="my-8 p-6 bg-purple-900/20 border-2 border-purple-500/30 rounded-xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 }}
>
<div className="flex items-start gap-4">
<Flag className="w-10 h-10 text-purple-400 flex-shrink-0 mt-1" />
<div>
<h3 className="text-xl font-bold text-purple-300 mb-2">Final Note on This Role</h3>
<p className="text-purple-200/80">
By applying, you acknowledge that you are stepping in as a strategic partner, not an employee. Your participation is based on ownership, contribution, and vision alignment. This is not employment. <strong>This is equity. This is ownership. This is legacy.</strong>
</p>
</div>
</div>
</motion.div>
);
const JobDetailPage = () => {
const { id } = useParams();
const navigate = useNavigate();
const { user, setShowAuthModal } = useAuth();
const [job, setJob] = useState(null);
const [loading, setLoading] = useState(true);
const [hasApplied, setHasApplied] = useState(false);
useEffect(() => {
let isMounted = true;
const fetchJob = async () => {
setLoading(true);
const { data, error } = await supabase
.from('job_openings')
.select('*')
.eq('id', id)
.single();
if (!isMounted) return;
if (error || !data) {
console.error('Error fetching job:', error);
navigate('/');
} else {
setJob(data);
}
};
const checkApplicationStatus = async () => {
if (!user) return;
const { data, error } = await supabase
.from('job_applications')
.select('id')
.eq('user_id', user.id)
.eq('job_id', id)
.maybeSingle(); // Use maybeSingle to avoid error if no application is found
if (!isMounted) return;
if (data) {
setHasApplied(true);
}
};
fetchJob().then(() => {
checkApplicationStatus().then(() => {
if (isMounted) setLoading(false);
});
});
return () => { isMounted = false; };
}, [id, navigate, user]);
if (loading) {
return (
<div className="flex justify-center items-center min-h-[60vh]">
<Loader2 className="w-12 h-12 text-primary animate-spin" />
</div>
);
}
if (!job) {
return null;
}
const handleApplyClick = () => {
if (!user) {
setShowAuthModal(true);
} else {
navigate(`/job/${job.id}/apply`);
}
};
return (
<>
<Helmet>
<title>{`${job.title} - AeThex Careers`}</title>
<meta name="description" content={job.description.substring(0, 160)} />
</Helmet>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="mb-8">
<Button asChild variant="ghost" className="mb-4">
<Link to="/" className="flex items-center text-gray-400 hover:text-white">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to All Roles
</Link>
</Button>
</div>
<div className="lg:grid lg:grid-cols-3 lg:gap-8">
<div className="lg:col-span-2">
<Card className="mb-8">
<CardHeader>
<CardDescription className="text-sm text-gray-400">Posted {timeAgo(job.posted_at)}</CardDescription>
<CardTitle className="text-3xl font-bold text-white">{job.title}</CardTitle>
<div className="flex items-center gap-4 pt-2 text-gray-300">
<span className="flex items-center gap-1.5"><Briefcase className="w-4 h-4 text-primary" /> {job.division} / {job.department}</span>
<span className="flex items-center gap-1.5"><MapPin className="w-4 h-4 text-primary" /> {job.location}</span>
</div>
</CardHeader>
<CardContent>
<div className="prose prose-invert prose-lg max-w-none text-gray-300 whitespace-pre-wrap">
{job.description}
</div>
</CardContent>
</Card>
<EquityDisclaimer />
<NdaDisclaimer />
<FinalNote />
</div>
<div className="lg:col-span-1">
<Card className="sticky top-24">
<CardHeader>
<CardTitle>Ready to Contribute?</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{hasApplied ? (
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/30">
<CheckCircle className="w-8 h-8 text-green-400" />
<div>
<h4 className="font-semibold text-white">Application Submitted</h4>
<p className="text-sm text-gray-300">We've received your application.</p>
</div>
</div>
) : (
<>
<p className="text-sm text-gray-400">
Submit your application to be considered for this role. We'll review your profile and get back to you soon.
</p>
<Button onClick={handleApplyClick} className="w-full" size="lg">
Apply Now
</Button>
</>
)}
{!user && !hasApplied && (
<div className="flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-300 text-sm">
<AlertCircle className="w-5 h-5 mt-0.5 flex-shrink-0" />
<p>
You must be <button onClick={() => setShowAuthModal(true)} className="font-bold underline hover:text-amber-200">signed in</button> to apply for this position.
</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</motion.div>
</>
);
};
export default JobDetailPage;

View file

@ -0,0 +1,146 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Loader2, FileText, ArrowLeft, AlertCircle } from 'lucide-react';
import { timeAgo } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
const getStatusVariant = (status) => {
switch (status.toLowerCase()) {
case 'submitted':
return 'default';
case 'under review':
return 'secondary';
case 'interviewing':
return 'success';
case 'rejected':
return 'destructive';
case 'hired':
return 'success';
default:
return 'outline';
}
};
const MyApplicationsPage = () => {
const { user } = useAuth();
const [applications, setApplications] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchApplications = async () => {
if (!user) {
setLoading(false);
return;
}
setLoading(true);
const { data, error } = await supabase
.from('job_applications')
.select(`
*,
job_openings (
title,
department,
location
)
`)
.eq('user_id', user.id)
.order('submitted_at', { ascending: false });
if (error) {
console.error('Error fetching applications:', error);
setApplications([]);
} else {
setApplications(data);
}
setLoading(false);
};
fetchApplications();
}, [user]);
return (
<>
<Helmet>
<title>My Applications - AeThex Careers</title>
<meta name="description" content="Track the status of your job applications with AeThex." />
</Helmet>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5 }}
>
<div className="mb-8 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-white">My Applications</h1>
<p className="text-gray-400 mt-1">Track the status of all your applications here.</p>
</div>
<Button asChild variant="outline">
<Link to="/" className="flex items-center">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Job Listings
</Link>
</Button>
</div>
<div className="bg-gray-900/50 border border-gray-800 rounded-lg overflow-hidden">
{loading ? (
<div className="flex justify-center items-center p-16">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : applications.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Position</TableHead>
<TableHead>Location</TableHead>
<TableHead>Submitted</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{applications.map((app) => (
<TableRow key={app.id}>
<TableCell className="font-medium">
<Link to={`/job/${app.job_id}`} className="hover:text-primary transition-colors">
{app.job_openings.title}
</Link>
<div className="text-xs text-gray-500">{app.job_openings.department}</div>
</TableCell>
<TableCell>{app.job_openings.location}</TableCell>
<TableCell>{timeAgo(app.submitted_at)}</TableCell>
<TableCell className="text-right">
<Badge variant={getStatusVariant(app.status)} className="capitalize">
{app.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-16 px-6">
<FileText className="w-16 h-16 mx-auto text-primary mb-4" />
<h2 className="text-2xl font-bold text-white mb-2">No Applications Yet</h2>
<p className="text-gray-400 mb-6">You haven't applied for any positions.</p>
<Button asChild>
<Link to="/">Explore Open Roles</Link>
</Button>
</div>
)}
</div>
</motion.div>
</>
);
};
export default MyApplicationsPage;

View file

@ -0,0 +1,151 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { AnimatePresence, motion } from 'framer-motion';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import EventCard from '@/components/EventCard';
import EventCardSkeleton from '@/components/EventCardSkeleton';
import EventDetailModal from '@/components/EventDetailModal';
import { useEvents } from '@/hooks/useEvents';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Ticket, ArrowLeft } from 'lucide-react';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const MyEventsPage = () => {
const { user } = useAuth();
const [myEvents, setMyEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedEvent, setSelectedEvent] = useState(null);
const {
getCategoryColor,
getCategoryLabel,
handleUnregister,
registrationLoading,
registeredEvents,
handleRegister
} = useEvents();
useEffect(() => {
const fetchMyEvents = async () => {
if (!user) {
setLoading(false);
return;
}
setLoading(true);
const { data, error } = await supabase
.from('aethex_event_registrations')
.select(`
aethex_events (
*,
aethex_event_registrations (count)
)
`)
.eq('user_id', user.id);
if (error) {
console.error('Error fetching my events:', error);
setMyEvents([]);
} else {
const formattedEvents = data.map(item => ({
...item.aethex_events,
registered: item.aethex_events.aethex_event_registrations[0]?.count || 0,
}));
setMyEvents(formattedEvents);
}
setLoading(false);
};
fetchMyEvents();
}, [user]);
return (
<>
<Helmet>
<title>My Registered Events - AeThex</title>
<meta name="description" content="View and manage all the AeThex events you are registered for." />
</Helmet>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5 }}
>
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white">My Registered Events</h1>
<p className="text-gray-400 mt-1">Here are all the upcoming events you've signed up for.</p>
</div>
<Button asChild variant="outline">
<Link to="/" className="flex items-center">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to All Events
</Link>
</Button>
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[...Array(3)].map((_, i) => <EventCardSkeleton key={i} />)}
</div>
) : myEvents.length > 0 ? (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
<AnimatePresence>
{myEvents.map((event) => (
<EventCard
key={event.id}
event={event}
onSelectEvent={setSelectedEvent}
getCategoryColor={getCategoryColor}
getCategoryLabel={getCategoryLabel}
/>
))}
</AnimatePresence>
</motion.div>
) : (
<div className="text-center py-16 bg-gray-900/30 rounded-2xl border border-gray-800">
<Ticket className="w-16 h-16 mx-auto text-primary mb-4" />
<h2 className="text-2xl font-bold text-white mb-2">No Registered Events</h2>
<p className="text-gray-400 mb-6">You haven't registered for any events yet.</p>
<Button asChild>
<Link to="/">Explore Events</Link>
</Button>
</div>
)}
</motion.div>
<AnimatePresence>
{selectedEvent && (
<EventDetailModal
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
onRegister={() => handleRegister(selectedEvent.id)}
onUnregister={handleUnregister}
isRegistered={registeredEvents.includes(selectedEvent.id)}
getCategoryColor={getCategoryColor}
getCategoryLabel={getCategoryLabel}
isLoading={registrationLoading}
/>
)}
</AnimatePresence>
</>
);
};
export default MyEventsPage;

View file

@ -0,0 +1,121 @@
import React from 'react';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Helmet } from 'react-helmet';
import { toast } from '@/components/ui/use-toast';
import { motion } from 'framer-motion';
import { Loader2 } from 'lucide-react';
const MyProfilePage = () => {
const { user, profile, loading } = useAuth();
const handleNotImplemented = () => {
toast({
title: "🚧 Feature in Development",
description: "This feature isn't implemented yet—but we're working on it! 🚀",
variant: "destructive"
});
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-[60vh]">
<Loader2 className="w-12 h-12 text-primary animate-spin" />
</div>
);
}
if (!profile) {
return (
<div className="text-center">
<p>Could not load profile.</p>
</div>
);
}
const avatarUrl = profile?.avatar_url || `https://api.dicebear.com/7.x/bottts/svg?seed=${user?.id}`;
return (
<>
<Helmet>
<title>My Profile - AeThex</title>
<meta name="description" content="Manage your AeThex contributor profile." />
</Helmet>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="flex flex-col md:flex-row items-start gap-8">
<motion.div
className="w-full md:w-1/3 lg:w-1/4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2, duration: 0.5 }}
>
<Card className="bg-slate-900/50 border-primary/20 backdrop-blur-sm overflow-hidden">
<CardHeader className="p-0">
<div className="relative h-40 bg-gradient-to-br from-primary/20 to-secondary/20">
<img
alt="Abstract banner for user profile"
className="w-full h-full object-cover"
src="https://images.unsplash.com/photo-1686140386811-099f53c0dd54" />
</div>
<div className="relative p-6 flex flex-col items-center -mt-16">
<div className="relative">
<img
src={avatarUrl}
alt={profile.username}
className="w-28 h-28 rounded-full border-4 border-slate-900 object-cover"
/>
<span className="absolute bottom-1 right-1 block h-5 w-5 rounded-full border-2 border-slate-900 bg-green-500" />
</div>
<CardTitle className="mt-4 text-2xl font-bold">{profile.full_name || profile.username}</CardTitle>
<p className="text-gray-400">@{profile.username}</p>
<p className="mt-2 text-center text-sm text-gray-300">{profile.bio}</p>
<Button onClick={handleNotImplemented} variant="outline" className="mt-4 w-full">Edit Profile</Button>
</div>
</CardHeader>
</Card>
</motion.div>
<motion.div
className="w-full md:w-2/3 lg:w-3/4"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4, duration: 0.5 }}
>
<Card className="bg-slate-900/50 border-primary/20 backdrop-blur-sm">
<CardHeader>
<CardTitle>Profile Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-400">Email</label>
<p className="text-lg">{profile.email}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-400">Role</label>
<p className="text-lg capitalize">{profile.role}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-400">Loyalty Points</label>
<p className="text-lg">{profile.loyalty_points}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-400">Joined</label>
<p className="text-lg">{new Date(profile.created_at).toLocaleDateString()}</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</motion.div>
</>
);
};
export default MyProfilePage;

View file

@ -0,0 +1,223 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { supabase } from '@/lib/customSupabaseClient';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { useSite } from '@/contexts/SiteContext';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Loader2, PlusCircle, Ticket } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { timeAgo } from '@/lib/utils';
const CreateTicketModal = ({ onTicketCreated }) => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [priority, setPriority] = useState('Medium');
const { user } = useAuth();
const { siteId } = useSite();
const { toast } = useToast();
const handleSubmit = async (e) => {
e.preventDefault();
if (!title || !description) {
toast({ variant: 'destructive', title: 'Missing fields', description: 'Please fill out all required fields.' });
return;
}
setLoading(true);
const { error } = await supabase.from('tickets').insert({
title,
description,
priority,
status: 'Open',
type: 'General',
created_by: user.id,
site_id: siteId,
});
setLoading(false);
if (error) {
toast({ variant: 'destructive', title: 'Error creating ticket', description: error.message });
} else {
toast({ variant: 'success', title: 'Ticket Created!', description: 'Your support ticket has been submitted.' });
setTitle('');
setDescription('');
setPriority('Medium');
onTicketCreated();
setOpen(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
Create New Ticket
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create a Support Ticket</DialogTitle>
<DialogDescription>
Describe the issue you're facing. Our team will get back to you as soon as possible.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">Title</Label>
<Input id="title" value={title} onChange={(e) => setTitle(e.target.value)} className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">Description</Label>
<Textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="priority" className="text-right">Priority</Label>
<Select value={priority} onValueChange={setPriority}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Submit Ticket
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
const MyTicketsPage = () => {
const [tickets, setTickets] = useState([]);
const [loading, setLoading] = useState(true);
const { user } = useAuth();
const { toast } = useToast();
const fetchTickets = useCallback(async () => {
if (!user) return;
setLoading(true);
const { data, error } = await supabase
.from('tickets')
.select('*')
.eq('created_by', user.id)
.order('created_at', { ascending: false });
if (error) {
toast({ variant: 'destructive', title: 'Error fetching tickets', description: error.message });
} else {
setTickets(data);
}
setLoading(false);
}, [user, toast]);
useEffect(() => {
fetchTickets();
}, [fetchTickets]);
const getStatusVariant = (status) => {
switch (status) {
case 'Open': return 'success';
case 'In Progress': return 'secondary';
case 'Closed': return 'outline';
default: return 'default';
}
};
const getPriorityVariant = (priority) => {
switch (priority) {
case 'High': return 'destructive';
case 'Medium': return 'warning';
case 'Low': return 'success';
default: return 'outline';
}
};
return (
<>
<Helmet>
<title>My Tickets | AeThex Contributor</title>
<meta name="description" content="View and manage your support tickets." />
</Helmet>
<div className="max-w-6xl mx-auto py-8 px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-white">My Support Tickets</h1>
<p className="text-gray-400 mt-1">Track the status of your support requests.</p>
</div>
<CreateTicketModal onTicketCreated={fetchTickets} />
</div>
<Card>
<CardHeader>
<CardTitle>Your Tickets</CardTitle>
<CardDescription>A list of all tickets you have submitted.</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center p-16">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : tickets.length === 0 ? (
<div className="text-center py-16">
<Ticket className="mx-auto h-12 w-12 text-gray-500" />
<h3 className="mt-2 text-lg font-medium text-white">No tickets found</h3>
<p className="mt-1 text-sm text-gray-400">Get started by creating a new ticket.</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Status</TableHead>
<TableHead>Priority</TableHead>
<TableHead className="text-right">Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tickets.map((ticket) => (
<TableRow key={ticket.id}>
<TableCell className="font-medium">{ticket.title}</TableCell>
<TableCell><Badge variant={getStatusVariant(ticket.status)}>{ticket.status}</Badge></TableCell>
<TableCell><Badge variant={getPriorityVariant(ticket.priority)}>{ticket.priority}</Badge></TableCell>
<TableCell className="text-right">{timeAgo(ticket.created_at)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</motion.div>
</div>
</>
);
};
export default MyTicketsPage;

View file

@ -0,0 +1,34 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
const NewsPage = () => {
// Placeholder content. In a real app, this would come from the blog_posts table.
return (
<>
<Helmet>
<title>News & Press | AeThex</title>
<meta name="description" content="The latest news, announcements, and press mentions from AeThex." />
</Helmet>
<div className="text-center mb-16">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono"
>
News & <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Updates</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto"
>
This section is under construction. Check back soon for the latest from AeThex.
</motion.p>
</div>
</>
);
};
export default NewsPage;

View file

@ -0,0 +1,138 @@
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { useNotifications } from '@/contexts/NotificationContext';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Loader2, Bell, MessageSquare, Briefcase, UserPlus } from 'lucide-react';
import { timeAgo } from '@/lib/utils';
import { useNavigate } from 'react-router-dom';
const NotificationIcon = ({ type }) => {
switch (type) {
case 'new_message':
return <MessageSquare className="w-6 h-6 text-blue-400" />;
case 'new_job_application':
return <Briefcase className="w-6 h-6 text-green-400" />;
case 'user_joined':
return <UserPlus className="w-6 h-6 text-purple-400" />;
default:
return <Bell className="w-6 h-6 text-gray-400" />;
}
};
const getNotificationDetails = (notification) => {
const { type, data } = notification;
switch(type) {
case 'new_message':
return {
text: <>New message from <strong>{data.sender_username}</strong>: "{data.message_preview}..."</>,
link: `/messages/${data.conversation_id}`
};
case 'new_job_application':
return {
text: <>New application for <strong>{data.job_title}</strong> from {data.applicant_username}.</>,
link: `/admin/applications/${data.application_id}`
};
case 'user_joined':
return {
text: <><strong>{data.username}</strong> just joined the platform!</>,
link: `/profile/${data.user_id}`
};
default:
return {
text: <>{data.message || 'New notification'}</>,
link: '#'
};
}
};
const NotificationsPage = () => {
const { notifications, loading, unreadCount, markAllAsRead, markAsRead } = useNotifications();
const navigate = useNavigate();
const handleNotificationClick = (notification) => {
const { link } = getNotificationDetails(notification);
if (!notification.is_read) {
markAsRead(notification.id);
}
if (link && link !== '#') {
navigate(link);
}
};
return (
<>
<Helmet>
<title>Notifications | AeThex Contributor</title>
<meta name="description" content="View all your notifications." />
</Helmet>
<div className="max-w-4xl mx-auto py-8 px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-white">All Notifications</h1>
<p className="text-gray-400 mt-1">Here's what you've missed.</p>
</div>
{unreadCount > 0 && (
<Button onClick={markAllAsRead}>Mark All as Read</Button>
)}
</div>
<Card>
<CardHeader>
<CardTitle>Your Alerts</CardTitle>
<CardDescription>A complete history of your notifications.</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center p-16">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : notifications.length === 0 ? (
<div className="text-center py-16">
<Bell className="mx-auto h-12 w-12 text-gray-500" />
<h3 className="mt-2 text-lg font-medium text-white">All caught up!</h3>
<p className="mt-1 text-sm text-gray-400">You have no new notifications.</p>
</div>
) : (
<div className="space-y-4">
{notifications.map((notification) => (
<div
key={notification.id}
onClick={() => handleNotificationClick(notification)}
className={`flex items-start gap-4 p-4 rounded-lg transition-colors cursor-pointer border ${
notification.is_read
? 'bg-gray-900/30 border-gray-800 hover:bg-gray-800/50'
: 'bg-primary/10 border-primary/20 hover:bg-primary/20'
}`}
>
<div className="flex-shrink-0 pt-1">
<NotificationIcon type={notification.type} />
</div>
<div className="flex-grow">
<p className="text-sm text-gray-200">{getNotificationDetails(notification).text}</p>
<p className="text-xs text-gray-400 mt-1">{timeAgo(notification.created_at)}</p>
</div>
{!notification.is_read && (
<div className="flex-shrink-0 self-center">
<div className="w-2.5 h-2.5 rounded-full bg-primary animate-pulse"></div>
</div>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</motion.div>
</div>
</>
);
};
export default NotificationsPage;

View file

@ -0,0 +1,78 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
const PrivacyPolicyPage = () => {
return (
<>
<Helmet>
<title>Privacy Policy | AeThex</title>
<meta name="description" content="Read the AeThex privacy policy to understand how we collect, store, and use your personal data to provide our services." />
<meta property="og:title" content="Privacy Policy | AeThex" />
<meta property="og:description" content="Learn how AeThex handles user data with a commitment to privacy and transparency." />
</Helmet>
<motion.div
className="prose prose-invert prose-lg max-w-4xl mx-auto py-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ staggerChildren: 0.2 }}
>
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
>
Privacy Policy
</motion.h1>
<motion.p
className="text-gray-400"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
Last updated: {new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</motion.p>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
AeThex ("us", "we", or "our") operates this website (the "Service"). This page informs you of our policies regarding the collection, use, and disclosure of personal data when you use our Service and the choices you have associated with that data. Our commitment is to user sovereignty and data minimization.
</motion.p>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }}>Information Collection and Use</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.4 }}>
We collect only the data that is essential for the functionality of our services and for securing our network. This includes:
</motion.p>
<motion.ul initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.5 }}>
<li><strong>Account Information:</strong> When you create an account, we may ask for information such as your username and email address to create and manage your contributor profile.</li>
<li><strong>Contact Form Submissions:</strong> When you contact us via our contact form, we collect your name, email, and message content to respond to your inquiries.</li>
<li><strong>Usage Data:</strong> We may collect anonymous data on how the Service is accessed and used to improve our offerings. This data is aggregated and cannot be used to identify you personally.</li>
</motion.ul>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.6 }}>Data Use</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.7 }}>
AeThex uses the collected data for various purposes:
</motion.p>
<motion.ul initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.8 }}>
<li>To provide and maintain our Service</li>
<li>To notify you about changes to our Service</li>
<li>To allow you to participate in interactive features of our Service when you choose to do so</li>
<li>To provide customer support</li>
<li>To monitor the usage of our Service to detect, prevent and address technical issues</li>
</motion.ul>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.9 }}>Data Security</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 1.0 }}>
The security of your data is important to us. We strive to use commercially acceptable means to protect your Personal Data, but remember that no method of transmission over the Internet or method of electronic storage is 100% secure. We do not sell, trade, or otherwise transfer your personally identifiable information to outside parties.
</motion.p>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 1.1 }}>Changes to This Privacy Policy</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 1.2 }}>
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. You are advised to review this Privacy Policy periodically for any changes.
</motion.p>
</motion.div>
</>
);
};
export default PrivacyPolicyPage;

View file

@ -0,0 +1,112 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
import { supabase } from '@/lib/customSupabaseClient';
import { Card, CardContent } from '@/components/ui/card';
import { Loader2, Linkedin, Twitter, Github } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
const TeamMemberCard = ({ member }) => {
const socialLinks = member.social_links || {};
return (
<Card className="text-center bg-gray-900/40 border-white/10 overflow-hidden transform hover:-translate-y-2 transition-transform duration-300">
<CardContent className="p-6">
<img
className="w-32 h-32 rounded-full mx-auto mb-4 border-4 border-primary/30 object-cover"
alt={`Portrait of ${member.name}, ${member.role}`}
src="https://images.unsplash.com/photo-1575383596664-30f4489f9786" />
<h3 className="text-xl font-bold text-white">{member.name}</h3>
<p className="text-primary font-semibold">{member.role}</p>
<p className="text-gray-400 mt-2 text-sm">{member.bio}</p>
<div className="flex justify-center gap-4 mt-4">
{socialLinks.linkedin && <a href={socialLinks.linkedin} target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-primary"><Linkedin /></a>}
{socialLinks.twitter && <a href={socialLinks.twitter} target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-primary"><Twitter /></a>}
{socialLinks.github && <a href={socialLinks.github} target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-primary"><Github /></a>}
</div>
</CardContent>
</Card>
);
};
const TeamPage = () => {
const [team, setTeam] = useState([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
useEffect(() => {
const fetchTeam = async () => {
const { data, error } = await supabase
.from('team_members')
.select('*')
.order('order_index', { ascending: true });
if (error) {
toast({
variant: "destructive",
title: "Failed to load team",
description: error.message,
});
} else {
setTeam(data);
}
setLoading(false);
};
fetchTeam();
}, [toast]);
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1 },
};
return (
<>
<Helmet>
<title>Our Team | AeThex</title>
<meta name="description" content="Meet the collective of visionaries, engineers, and architects building the future at AeThex." />
<meta property="og:title" content="Our Team | AeThex" />
<meta property="og:description" content="Meet the collective of visionaries, engineers, and architects building the future at AeThex." />
</Helmet>
<motion.div
initial="hidden"
animate="visible"
variants={containerVariants}
>
<div className="text-center mb-16">
<motion.h1 variants={itemVariants} className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono">
Our <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Collective</span>
</motion.h1>
<motion.p variants={itemVariants} className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
We are a distributed team of researchers, engineers, and strategists united by a shared vision for a decentralized future.
</motion.p>
</div>
{loading ? (
<div className="flex justify-center items-center py-20">
<Loader2 className="w-12 h-12 text-primary animate-spin" />
</div>
) : (
<motion.div
variants={containerVariants}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{team.map((member) => (
<motion.div key={member.id} variants={itemVariants}>
<TeamMemberCard member={member} />
</motion.div>
))}
</motion.div>
)}
</motion.div>
</>
);
};
export default TeamPage;

View file

@ -0,0 +1,34 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
const TechnologyPage = () => {
// Placeholder content. In a real app, this would come from a CMS or the database.
return (
<>
<Helmet>
<title>Technology | AeThex</title>
<meta name="description" content="Explore the core technologies and research areas that power AeThex's vision for a decentralized future." />
</Helmet>
<div className="text-center mb-16">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 font-mono"
>
Our <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Technology</span> Stack
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto"
>
A brief overview of the technological pillars we are building upon. This page is currently a placeholder.
</motion.p>
</div>
</>
);
};
export default TechnologyPage;

View file

@ -0,0 +1,73 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { motion } from 'framer-motion';
const TermsAndConditionsPage = () => {
return (
<>
<Helmet>
<title>Terms and Conditions | AeThex</title>
<meta name="description" content="Read the AeThex Terms and Conditions to understand the rules and guidelines for using our website and services." />
<meta property="og:title" content="Terms and Conditions | AeThex" />
<meta property="og:description" content="Review the terms of service for the AeThex platform." />
</Helmet>
<motion.div
className="prose prose-invert prose-lg max-w-4xl mx-auto py-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ staggerChildren: 0.2 }}
>
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
>
Terms and Conditions
</motion.h1>
<motion.p
className="text-gray-400"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
Last updated: {new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</motion.p>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
Welcome to AeThex. These terms and conditions outline the rules and regulations for the use of our website. By accessing this website, we assume you accept these terms and conditions. Do not continue to use AeThex if you do not agree to all of the terms and conditions stated on this page.
</motion.p>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }}>1. Intellectual Property Rights</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.4 }}>
Other than the content you own, under these Terms, AeThex and/or its licensors own all the intellectual property rights and materials contained in this Website. You are granted a limited license only for purposes of viewing the material contained on this Website. Contributor-owned content remains the property of the contributor.
</motion.p>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.5 }}>2. Restrictions</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.6 }}>
You are specifically restricted from all of the following:
</motion.p>
<motion.ul initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.7 }}>
<li>Publishing any Website material in any other media without citation.</li>
<li>Selling, sublicensing and/or otherwise commercializing any Website material.</li>
<li>Using this Website in any way that is or may be damaging to this Website.</li>
<li>Using this Website contrary to applicable laws and regulations, or in any way may cause harm to the Website, or to any person or business entity.</li>
<li>Engaging in any data mining, data harvesting, data extracting or any other similar activity in relation to this Website.</li>
</motion.ul>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.8 }}>3. Limitation of Liability</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.9 }}>
In no event shall AeThex, nor any of its officers, directors and contributors, be held liable for anything arising out of or in any way connected with your use of this Website whether such liability is under contract. AeThex, including its officers, directors and contributors shall not be held liable for any indirect, consequential or special liability arising out of or in any way related to your use of this Website.
</motion.p>
<motion.h2 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 1.0 }}>4. Governing Law & Jurisdiction</motion.h2>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 1.1 }}>
These Terms will be governed by and interpreted in accordance with the laws of the jurisdiction in which the company is registered, and you submit to the non-exclusive jurisdiction of the state and federal courts located in such jurisdiction for the resolution of any disputes.
</motion.p>
</motion.div>
</>
);
};
export default TermsAndConditionsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminAnnouncementsPage = () => {
return null;
};
export default AdminAnnouncementsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminApplicationsPage = () => {
return null;
};
export default AdminApplicationsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminBlogPage = () => {
return null;
};
export default AdminBlogPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminCandidatesPage = () => {
return null;
};
export default AdminCandidatesPage;

View file

@ -0,0 +1,96 @@
import React, { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { useSite } from '@/contexts/SiteContext';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
const AdminContributorsPage = () => {
const [contributors, setContributors] = useState([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const { siteId } = useSite();
const fetchContributors = useCallback(async () => {
if (!siteId) return;
setLoading(true);
const { data, error } = await supabase.rpc('get_all_users_for_site', { p_site_id: siteId });
if (error) {
console.error('Error fetching contributors:', error);
toast({ variant: 'destructive', title: 'Error', description: 'Could not fetch contributors.' });
} else {
setContributors(data);
}
setLoading(false);
}, [toast, siteId]);
useEffect(() => {
fetchContributors();
}, [fetchContributors]);
const getRoleVariant = (role) => {
switch (role) {
case 'site_owner': return 'default';
case 'admin': return 'secondary';
case 'oversee': return 'destructive';
default: return 'outline';
}
}
return (
<>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-white">Contributors</h1>
<p className="text-gray-400 mt-1">Manage all users contributing to this site.</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>All Contributors</CardTitle>
<CardDescription>A total of {contributors.length} contributors.</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center p-16">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead className="text-right">Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{contributors.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-3">
<img src={user.avatar_url} alt={user.username} className="w-8 h-8 rounded-full" />
{user.username}
</div>
</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell className="text-right">
<Badge variant={getRoleVariant(user.role)} className="capitalize">{user.role.replace('_', ' ')}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</>
);
};
export default AdminContributorsPage;

View file

@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Users, FolderGit2, Ticket, Loader2 } from 'lucide-react';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import { supabase } from '@/lib/customSupabaseClient';
import { useSite } from '@/contexts/SiteContext';
const StatCard = ({ title, value, icon: Icon, loading }) => {
return (
<Card className="bg-gray-900/40 border-white/10">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-400">{title}</CardTitle>
<Icon className="h-5 w-5 text-gray-500" />
</CardHeader>
<CardContent>
{loading ? (
<Loader2 className="h-6 w-6 animate-spin text-primary" />
) : (
<div className="text-2xl font-bold text-white">{value}</div>
)}
</CardContent>
</Card>
);
};
const AdminDashboardPage = () => {
const { profile } = useAuth();
const { siteId } = useSite();
const [stats, setStats] = useState({ totalUsers: 0, activeProjects: 0, openTickets: 0 });
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStats = async () => {
if (!siteId) return;
setLoading(true);
const { data, error } = await supabase.rpc('get_employee_management_stats', { p_site_id: siteId });
if (error) {
console.error("Error fetching dashboard stats:", error);
} else {
setStats(data);
}
setLoading(false);
};
fetchStats();
}, [siteId]);
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold text-white">Welcome, {profile?.username || 'Admin'}!</h1>
<p className="text-gray-400">Here's an overview of your contributor site.</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<StatCard title="Total Contributors" value={stats.totalUsers} icon={Users} loading={loading} />
<StatCard title="Active Projects" value={stats.activeProjects} icon={FolderGit2} loading={loading} />
<StatCard title="Open Tickets" value={stats.openTickets} icon={Ticket} loading={loading} />
</div>
<div className="mt-8 text-center text-gray-500">
<p>More detailed analytics will be available here soon.</p>
</div>
</div>
);
};
export default AdminDashboardPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminDocumentsPage = () => {
return null;
};
export default AdminDocumentsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminEventsPage = () => {
return null;
};
export default AdminEventsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminJobsPage = () => {
return null;
};
export default AdminJobsPage;

View file

@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { NavLink, Outlet, Link } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { LayoutDashboard, Settings, Menu, X, Users, FolderGit2, Ticket } from 'lucide-react';
import { Button } from '@/components/ui/button';
import AeThexLogo from '@/components/AeThexLogo';
import { useAuth } from '@/contexts/SupabaseAuthContext';
import NotificationBell from '@/components/NotificationBell';
const navItems = [
{ to: '/admin', icon: LayoutDashboard, label: 'Dashboard' },
{ to: '/admin/contributors', icon: Users, label: 'Contributors' },
{ to: '/admin/projects', icon: FolderGit2, label: 'Projects' },
{ to: '/admin/tickets', icon: Ticket, label: 'Tickets' },
{ to: '/admin/settings', icon: Settings, label: 'Settings' },
];
const Sidebar = ({ isMobile, closeSidebar }) => (
<aside className={`flex flex-col bg-slate-950/80 backdrop-blur-lg border-r border-white/10 ${isMobile ? 'w-64' : 'lg:w-64'}`}>
<div className="p-4 flex items-center justify-between border-b border-white/10">
<Link to="/" className="flex items-center gap-2">
<AeThexLogo className="h-8" />
<span className="font-bold text-lg">Contributor Admin</span>
</Link>
{isMobile && (
<Button variant="ghost" size="icon" onClick={closeSidebar}>
<X className="h-6 w-6" />
</Button>
)}
</div>
<nav className="flex-1 px-2 py-4 space-y-1">
{navItems.map((item) => (
<NavLink
key={item.label}
to={item.to}
end={item.to === '/admin'}
onClick={isMobile ? closeSidebar : undefined}
className={({ isActive }) =>
`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 ${
isActive
? 'bg-primary/20 text-primary'
: 'text-gray-300 hover:bg-slate-800/50 hover:text-white'
}`
}
>
<item.icon className="mr-3 h-5 w-5" />
{item.label}
</NavLink>
))}
</nav>
</aside>
);
const AdminLayout = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const { profile } = useAuth();
const avatarUrl = profile?.avatar_url || `https://api.dicebear.com/7.x/bottts/svg?seed=${profile?.id}`;
return (
<div className="flex h-screen bg-slate-900 text-white font-mono">
<div className="hidden lg:flex lg:flex-shrink-0">
<Sidebar />
</div>
<AnimatePresence>
{isSidebarOpen && (
<motion.div
initial={{ x: '-100%' }}
animate={{ x: 0 }}
exit={{ x: '-100%' }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className="fixed inset-y-0 left-0 z-50 lg:hidden"
>
<Sidebar isMobile closeSidebar={() => setIsSidebarOpen(false)} />
</motion.div>
)}
</AnimatePresence>
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<header className="sticky top-0 z-30 lg:hidden flex items-center justify-between p-2 bg-slate-950/80 backdrop-blur-lg border-b border-white/10">
<Button variant="ghost" size="icon" onClick={() => setIsSidebarOpen(true)}>
<Menu className="h-6 w-6" />
</Button>
<div className="flex items-center gap-4">
<NotificationBell />
<img
src={avatarUrl}
alt="User Avatar"
className="h-8 w-8 rounded-full"
/>
</div>
</header>
<main className="flex-1 relative overflow-y-auto focus:outline-none bg-grid-pattern">
<div className="py-8 px-4 sm:px-6 lg:px-8">
<Outlet />
</div>
</main>
</div>
</div>
);
};
export default AdminLayout;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminPagesPage = () => {
return null;
};
export default AdminPagesPage;

View file

@ -0,0 +1,103 @@
import React, { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { useSite } from '@/contexts/SiteContext';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { timeAgo } from '@/lib/utils';
const AdminProjectsPage = () => {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const { siteId } = useSite();
const fetchProjects = useCallback(async () => {
if (!siteId) return;
setLoading(true);
const { data, error } = await supabase
.from('projects')
.select('*, owner:profiles(username, avatar_url)')
.eq('site_id', siteId)
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching projects:', error);
toast({ variant: 'destructive', title: 'Error', description: 'Could not fetch projects.' });
} else {
setProjects(data);
}
setLoading(false);
}, [toast, siteId]);
useEffect(() => {
fetchProjects();
}, [fetchProjects]);
const getStatusVariant = (status) => {
switch (status) {
case 'In Progress': return 'success';
case 'Completed': return 'default';
case 'On Hold': return 'secondary';
default: return 'outline';
}
}
return (
<>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-white">Projects</h1>
<p className="text-gray-400 mt-1">Oversee all contributor projects.</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>All Projects</CardTitle>
<CardDescription>A total of {projects.length} projects.</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center p-16">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Owner</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projects.map((project) => (
<TableRow key={project.id}>
<TableCell className="font-medium">{project.title}</TableCell>
<TableCell>
<div className="flex items-center gap-3">
<img src={project.owner.avatar_url} alt={project.owner.username} className="w-8 h-8 rounded-full" />
{project.owner.username}
</div>
</TableCell>
<TableCell>{timeAgo(project.created_at)}</TableCell>
<TableCell className="text-right">
<Badge variant={getStatusVariant(project.status)}>{project.status}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</>
);
};
export default AdminProjectsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminProspectsPage = () => {
return null;
};
export default AdminProspectsPage;

View file

@ -0,0 +1,151 @@
import React, { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { useSite } from '@/contexts/SiteContext';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent, CardDescription, CardFooter } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Loader2 } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
const AdminSettingsPage = () => {
const [editableSettings, setEditableSettings] = useState(null);
const [saving, setSaving] = useState(false);
const { toast } = useToast();
const { siteId, siteConfig, loading: siteLoading, refreshSiteConfig } = useSite();
useEffect(() => {
if (siteConfig) {
setEditableSettings(siteConfig);
}
}, [siteConfig]);
const handleSave = useCallback(async (settingsToSave) => {
if (!siteId) {
toast({ variant: 'destructive', title: 'Error', description: 'Site ID is missing. Cannot save settings.' });
return;
}
setSaving(true);
try {
const { error } = await supabase
.from('site_config')
.update(settingsToSave)
.eq('site_id', siteId)
.select()
.single();
if (error) throw error;
toast({ title: 'Settings saved successfully' });
await refreshSiteConfig();
} catch (error) {
toast({ variant: 'destructive', title: 'Error saving settings', description: error.message });
} finally {
setSaving(false);
}
}, [siteId, refreshSiteConfig, toast]);
const handleInputChange = (e) => {
const { id, value } = e.target;
setEditableSettings(prev => ({ ...prev, [id]: value }));
};
const handleSwitchChange = (checked) => {
const newStatus = checked ? 'maintenance' : 'online';
const newSettings = { ...editableSettings, system_status: newStatus };
setEditableSettings(newSettings);
handleSave({ system_status: newStatus });
};
const handleGeneralSave = () => {
if (!editableSettings) return;
const settingsToSave = {
site_name: editableSettings.site_name,
site_description: editableSettings.site_description
};
handleSave(settingsToSave);
};
const handleMessageSave = () => {
if (!editableSettings) return;
const settingsToSave = {
system_status_message: editableSettings.system_status_message
};
handleSave(settingsToSave);
};
if (siteLoading || !editableSettings) {
return <div className="flex justify-center items-center h-full"><Loader2 className="h-8 w-8 animate-spin text-primary" /></div>;
}
const isMaintenanceMode = editableSettings.system_status === 'maintenance';
return (
<div>
<h1 className="text-3xl font-bold text-white mb-8">Site Settings</h1>
<div className="grid gap-8">
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle>General Settings</CardTitle>
<CardDescription>Manage general site information and settings.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="site_id">Site ID</Label>
<Input id="site_id" value={siteId || ''} readOnly disabled className="font-mono text-gray-400" />
</div>
<div className="space-y-2">
<Label htmlFor="site_name">Site Name</Label>
<Input id="site_name" value={editableSettings.site_name || ''} onChange={handleInputChange} disabled={saving} />
</div>
<div className="space-y-2">
<Label htmlFor="site_description">Site Description</Label>
<Textarea id="site_description" value={editableSettings.site_description || ''} onChange={handleInputChange} disabled={saving} />
</div>
</CardContent>
<CardFooter className="border-t border-white/10 px-6 py-4">
<Button onClick={handleGeneralSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save General Settings
</Button>
</CardFooter>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle>Maintenance Mode</CardTitle>
<CardDescription>Put the site in maintenance mode to perform updates.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Switch
id="maintenance-mode"
checked={isMaintenanceMode}
onCheckedChange={handleSwitchChange}
disabled={saving}
/>
<Label htmlFor="maintenance-mode">Enable Maintenance Mode</Label>
</div>
{isMaintenanceMode && (
<div className="space-y-2">
<Label htmlFor="system_status_message">Maintenance Message</Label>
<Textarea id="system_status_message" placeholder="e.g., We'll be back shortly!" value={editableSettings.system_status_message || ''} onChange={handleInputChange} disabled={saving} />
</div>
)}
</CardContent>
<CardFooter className="border-t border-white/10 px-6 py-4">
<Button onClick={handleMessageSave} disabled={saving || !isMaintenanceMode}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Maintenance Message
</Button>
</CardFooter>
</Card>
</div>
</div>
);
};
export default AdminSettingsPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminTeamPage = () => {
return null;
};
export default AdminTeamPage;

View file

@ -0,0 +1,120 @@
import React, { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { useSite } from '@/contexts/SiteContext';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { timeAgo } from '@/lib/utils';
const AdminTicketsPage = () => {
const [tickets, setTickets] = useState([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const { siteId } = useSite();
const fetchTickets = useCallback(async () => {
if (!siteId) return;
setLoading(true);
const { data, error } = await supabase
.from('tickets')
.select(`
*,
created_by:profiles!tickets_created_by_fkey(username, avatar_url),
assigned_to:profiles!tickets_assigned_to_fkey(username, avatar_url)
`)
.eq('site_id', siteId)
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching tickets:', error);
toast({ variant: 'destructive', title: 'Error', description: 'Could not fetch tickets.' });
} else {
setTickets(data);
}
setLoading(false);
}, [toast, siteId]);
useEffect(() => {
fetchTickets();
}, [fetchTickets]);
const getStatusVariant = (status) => {
switch (status) {
case 'Open': return 'success';
case 'In Progress': return 'secondary';
case 'Closed': return 'outline';
default: return 'default';
}
}
const getPriorityVariant = (priority) => {
switch (priority) {
case 'High': return 'destructive';
case 'Medium': return 'warning';
case 'Low': return 'success';
default: return 'outline';
}
}
return (
<>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-white">Tickets</h1>
<p className="text-gray-400 mt-1">Track and resolve contributor support tickets.</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>All Tickets</CardTitle>
<CardDescription>A total of {tickets.length} tickets.</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center p-16">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Submitter</TableHead>
<TableHead>Created</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Priority</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tickets.map((ticket) => (
<TableRow key={ticket.id}>
<TableCell className="font-medium">{ticket.title}</TableCell>
<TableCell>
{ticket.created_by ? (
<div className="flex items-center gap-3">
<img src={ticket.created_by.avatar_url} alt={ticket.created_by.username} className="w-8 h-8 rounded-full" />
{ticket.created_by.username}
</div>
) : 'N/A'}
</TableCell>
<TableCell>{timeAgo(ticket.created_at)}</TableCell>
<TableCell><Badge variant={getStatusVariant(ticket.status)}>{ticket.status}</Badge></TableCell>
<TableCell className="text-right">
<Badge variant={getPriorityVariant(ticket.priority)}>{ticket.priority}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</>
);
};
export default AdminTicketsPage;

View file

@ -0,0 +1,118 @@
import React, { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/customSupabaseClient';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Loader2, Check, X, MoreHorizontal } from 'lucide-react';
import { formatDate } from '@/lib/utils';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
const AdminTimeOffPage = () => {
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const fetchRequests = useCallback(async () => {
setLoading(true);
const { data, error } = await supabase
.from('time_off_requests')
.select('*, profiles(username, avatar_url)')
.order('created_at', { ascending: false });
if (error) {
toast({ variant: 'destructive', title: 'Error fetching time off requests', description: error.message });
} else {
setRequests(data);
}
setLoading(false);
}, [toast]);
useEffect(() => {
fetchRequests();
}, [fetchRequests]);
const handleUpdateStatus = async (id, status) => {
const { error } = await supabase
.from('time_off_requests')
.update({ status })
.eq('id', id);
if (error) {
toast({ variant: 'destructive', title: 'Error updating status', description: error.message });
} else {
toast({ variant: 'success', title: 'Status Updated' });
fetchRequests();
}
};
const getStatusVariant = (status) => {
switch (status.toLowerCase()) {
case 'approved': return 'success';
case 'rejected': return 'destructive';
case 'pending': return 'warning';
default: return 'outline';
}
};
return (
<div>
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-white">Time Off Requests</h1>
<p className="text-gray-400 mt-1">Review and manage employee time off requests.</p>
</div>
</div>
<div className="border border-white/10 rounded-lg overflow-hidden bg-gray-950/20 backdrop-blur-lg">
<Table>
<TableHeader>
<TableRow className="border-b-white/10 hover:bg-transparent">
<TableHead className="text-white/80">Employee</TableHead>
<TableHead className="text-white/80">Dates</TableHead>
<TableHead className="text-white/80">Reason</TableHead>
<TableHead className="text-center text-white/80">Status</TableHead>
<TableHead className="text-right text-white/80">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan="5" className="text-center py-16"><Loader2 className="mx-auto h-8 w-8 animate-spin text-primary" /></TableCell></TableRow>
) : requests.length === 0 ? (
<TableRow><TableCell colSpan="5" className="text-center py-16 text-gray-500">No time off requests found.</TableCell></TableRow>
) : (
requests.map((req) => (
<TableRow key={req.id} className="border-b-white/10 last:border-b-0 hover:bg-white/5">
<TableCell className="font-medium text-white">
<div className="flex items-center gap-3">
<img src={req.profiles.avatar_url} alt={req.profiles.username} className="w-8 h-8 rounded-full" />
{req.profiles.username}
</div>
</TableCell>
<TableCell className="text-gray-300">{formatDate(req.start_date)} - {formatDate(req.end_date)}</TableCell>
<TableCell className="text-gray-300 max-w-xs truncate">{req.reason}</TableCell>
<TableCell className="text-center"><Badge variant={getStatusVariant(req.status)} className="capitalize">{req.status}</Badge></TableCell>
<TableCell className="text-right">
{req.status === 'pending' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"><MoreHorizontal className="h-4 w-4" /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-gray-900 border-white/10 text-white">
<DropdownMenuItem onClick={() => handleUpdateStatus(req.id, 'approved')} className="text-green-400 focus:text-green-400 cursor-pointer"><Check className="mr-2 h-4 w-4" /> Approve</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleUpdateStatus(req.id, 'rejected')} className="text-red-400 focus:text-red-400 cursor-pointer"><X className="mr-2 h-4 w-4" /> Reject</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
};
export default AdminTimeOffPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminUsersPage = () => {
return null;
};
export default AdminUsersPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const AdminWaitlistPage = () => {
return null;
};
export default AdminWaitlistPage;

View file

@ -0,0 +1,7 @@
import React from 'react';
const EventFormModal = () => {
return null;
};
export default EventFormModal;

View file

@ -0,0 +1,7 @@
import React from 'react';
const JobFormModal = () => {
return null;
};
export default JobFormModal;

View file

@ -0,0 +1,87 @@
/** @type {import('tailwindcss').Config} */
const { fontFamily } = require('tailwindcss/defaultTheme')
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{js,jsx}',
'./components/**/*.{js,jsx}',
'./app/**/*.{js,jsx}',
'./src/**/*.{js,jsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: `calc(var(--radius) - 4px)`,
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"aurora-flow": {
"0%": { transform: "translate(-50%, -50%) rotate(0deg)" },
"100%": { transform: "translate(-50%, -50%) rotate(360deg)" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"aurora-flow": "aurora-flow 20s linear infinite",
},
fontFamily: {
sans: ['Inter', ...fontFamily.sans],
mono: ['Courier Prime', 'Courier New', ...fontFamily.mono],
},
},
},
plugins: [require("tailwindcss-animate"), require('@tailwindcss/typography')],
};

View file

@ -0,0 +1,184 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
const CLEAN_CONTENT_REGEX = {
comments: /\/\*[\s\S]*?\*\/|\/\/.*$/gm,
templateLiterals: /`[\s\S]*?`/g,
strings: /'[^']*'|"[^"]*"/g,
jsxExpressions: /\{.*?\}/g,
htmlEntities: {
quot: /&quot;/g,
amp: /&amp;/g,
lt: /&lt;/g,
gt: /&gt;/g,
apos: /&apos;/g
}
};
const EXTRACTION_REGEX = {
route: /<Route\s+[^>]*>/g,
path: /path=["']([^"']+)["']/,
element: /element=\{<(\w+)[^}]*\/?\s*>\}/,
helmet: /<Helmet[^>]*?>([\s\S]*?)<\/Helmet>/i,
helmetTest: /<Helmet[\s\S]*?<\/Helmet>/i,
title: /<title[^>]*?>\s*(.*?)\s*<\/title>/i,
description: /<meta\s+name=["']description["']\s+content=["'](.*?)["']/i
};
function cleanContent(content) {
return content
.replace(CLEAN_CONTENT_REGEX.comments, '')
.replace(CLEAN_CONTENT_REGEX.templateLiterals, '""')
.replace(CLEAN_CONTENT_REGEX.strings, '""');
}
function cleanText(text) {
if (!text) return text;
return text
.replace(CLEAN_CONTENT_REGEX.jsxExpressions, '')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.quot, '"')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.amp, '&')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.lt, '<')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.gt, '>')
.replace(CLEAN_CONTENT_REGEX.htmlEntities.apos, "'")
.trim();
}
function extractRoutes(appJsxPath) {
if (!fs.existsSync(appJsxPath)) return new Map();
try {
const content = fs.readFileSync(appJsxPath, 'utf8');
const routes = new Map();
const routeMatches = [...content.matchAll(EXTRACTION_REGEX.route)];
for (const match of routeMatches) {
const routeTag = match[0];
const pathMatch = routeTag.match(EXTRACTION_REGEX.path);
const elementMatch = routeTag.match(EXTRACTION_REGEX.element);
const isIndex = routeTag.includes('index');
if (elementMatch) {
const componentName = elementMatch[1];
let routePath;
if (isIndex) {
routePath = '/';
} else if (pathMatch) {
routePath = pathMatch[1].startsWith('/') ? pathMatch[1] : `/${pathMatch[1]}`;
}
routes.set(componentName, routePath);
}
}
return routes;
} catch (error) {
return new Map();
}
}
function findReactFiles(dir) {
return fs.readdirSync(dir)
.map(item => path.join(dir, item))
.filter(itemPath => fs.statSync(itemPath).isFile());
}
function extractHelmetData(content, filePath, routes) {
const cleanedContent = cleanContent(content);
if (!EXTRACTION_REGEX.helmetTest.test(cleanedContent)) {
return null;
}
const helmetMatch = content.match(EXTRACTION_REGEX.helmet);
if (!helmetMatch) return null;
const helmetContent = helmetMatch[1];
const titleMatch = helmetContent.match(EXTRACTION_REGEX.title);
const descMatch = helmetContent.match(EXTRACTION_REGEX.description);
const title = cleanText(titleMatch?.[1]);
const description = cleanText(descMatch?.[1]);
const fileName = path.basename(filePath, path.extname(filePath));
const url = routes.length && routes.has(fileName)
? routes.get(fileName)
: generateFallbackUrl(fileName);
return {
url,
title: title || 'Untitled Page',
description: description || 'No description available'
};
}
function generateFallbackUrl(fileName) {
const cleanName = fileName.replace(/Page$/, '').toLowerCase();
return cleanName === 'app' ? '/' : `/${cleanName}`;
}
function generateLlmsTxt(pages) {
const sortedPages = pages.sort((a, b) => a.title.localeCompare(b.title));
const pageEntries = sortedPages.map(page =>
`- [${page.title}](${page.url}): ${page.description}`
).join('\n');
return `## Pages\n${pageEntries}`;
}
function ensureDirectoryExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function processPageFile(filePath, routes) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return extractHelmetData(content, filePath, routes);
} catch (error) {
console.error(`❌ Error processing ${filePath}:`, error.message);
return null;
}
}
function main() {
const pagesDir = path.join(process.cwd(), 'src', 'pages');
const appJsxPath = path.join(process.cwd(), 'src', 'App.jsx');
let pages = [];
if (!fs.existsSync(pagesDir)) {
pages.push(processPageFile(appJsxPath, []))
pages = pages.filter(Boolean);
} else {
const routes = extractRoutes(appJsxPath);
const reactFiles = findReactFiles(pagesDir);
pages = reactFiles
.map(filePath => processPageFile(filePath, routes))
.filter(Boolean);
}
if (pages.length === 0) {
console.error('❌ No pages with Helmet components found!');
process.exit(1);
}
const llmsTxtContent = generateLlmsTxt(pages);
const outputPath = path.join(process.cwd(), 'public', 'llms.txt');
ensureDirectoryExists(path.dirname(outputPath));
fs.writeFileSync(outputPath, llmsTxtContent, 'utf8');
}
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
main();
}

Some files were not shown because too many files have changed in this diff Show more