diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f87db9b --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# GoToSocial React Frontend Configuration + +# Optional: Default instance URL (can be overridden at login) +# REACT_APP_DEFAULT_INSTANCE_URL=https://your-gotosocial-instance.com + +# Optional: Application name (used in OAuth registration) +# REACT_APP_CLIENT_NAME=GoToSocial React Frontend + +# Optional: Enable development mode features +# REACT_APP_DEV_MODE=true \ No newline at end of file diff --git a/README.md b/README.md index 58beeac..741e14d 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,164 @@ -# Getting Started with Create React App +# GoToSocial React Frontend -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +Un frontend moderno y responsivo en React para GoToSocial - un servidor de red social ActivityPub rápido, divertido y pequeño. -## Available Scripts +## ✨ Características -In the project directory, you can run: +### 🎨 Diseño Moderno +- Interfaz moderna basada en gradientes con estética limpia y profesional +- Diseño totalmente responsivo que funciona en desktop, tablet y móvil +- Navegación intuitiva con jerarquía visual clara +- Animaciones y transiciones suaves -### `npm start` +### 🏠 Vistas de Timeline +- **Home Timeline**: Timeline personalizado para usuarios autenticados +- **Public Timeline**: Posts públicos globales de instancias federadas +- **Local Timeline**: Posts públicos solo de la instancia local +- Scroll infinito con funcionalidad de cargar más -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +### 👤 Perfiles de Usuario +- Páginas de perfil completas con avatar, biografía y estadísticas +- Pestañas de posts, medios e interacciones +- Funcionalidad de seguir/no seguir +- Estadísticas de perfil (seguidores, siguiendo, posts) -The page will reload when you make changes.\ -You may also see any lint errors in the console. +### 📝 Gestión de Posts +- Compositor de posts rico con controles de privacidad +- **Subida de medios e imágenes** con previsualización y edición de texto alternativo +- **Selector de emojis** con emojis comunes y inserción en posición del cursor +- Soporte para advertencias de contenido y archivos adjuntos +- Visualización completa de posts con interacciones (like, boost, reply, share) +- Conversaciones anidadas y cadenas de respuestas -### `npm test` +### 🔔 Notificaciones +- Sistema de notificaciones en tiempo real +- Notificaciones categorizadas (likes, boosts, follows, menciones) +- Indicadores visuales para notificaciones no leídas -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +### 🔍 Búsqueda +- Búsqueda universal a través de cuentas, hashtags y posts +- Resultados de búsqueda con pestañas para filtrado fácil +- Sugerencias de búsqueda en tiempo real -### `npm run build` +### 🔐 Autenticación OAuth2 +- Sistema de autenticación OAuth2 completamente funcional +- Registro automático de aplicación en instancias GoToSocial +- Intercambio seguro de códigos de autorización por tokens +- Gestión automática de refresh de tokens +- Sesiones persistentes con almacenamiento local +- Configuración de URL de instancia -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +## Tech Stack -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +- **React 19** - Modern React with latest features +- **React Router DOM** - Client-side routing +- **Styled Components** - CSS-in-JS for component styling +- **Axios** - HTTP client for API requests +- **React Icons** - Beautiful icon library -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +## Getting Started -### `npm run eject` +### Prerequisites +- Node.js 16 or higher +- npm or yarn -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +### Installation -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +1. Install dependencies: + ```bash + npm install + ``` -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +2. Start the development server: + ```bash + npm start + ``` -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +3. Open [http://localhost:3000](http://localhost:3000) in your browser + +### Autenticación OAuth2 + +El frontend utiliza OAuth2 para autenticación segura: + +1. Ingresa la URL de tu instancia GoToSocial o Mastodon +2. Haz clic en "Conectar con OAuth" +3. Serás redirigido a la página de autorización de tu instancia +4. Autoriza la aplicación +5. Serás redirigido de vuelta e iniciarás sesión automáticamente + +### Funcionalidades de Medios y Emojis + +- **Subida de Imágenes**: Selecciona hasta 4 imágenes con límite de 10MB por archivo +- **Previsualización de Medios**: Vista previa de imágenes antes de publicar +- **Texto Alternativo**: Edita el texto alternativo para accesibilidad +- **Selector de Emojis**: Más de 100 emojis comunes organizados para fácil selección +- **Inserción Inteligente**: Los emojis se insertan en la posición exacta del cursor + +## Configuration + +### Environment Variables + +Create a `.env` file in the root directory: + +```env +REACT_APP_API_BASE_URL=https://your-gotosocial-instance.com +``` + +### Production Deployment + +For production deployment: + +1. Build the application: `npm run build` +2. Serve the built files from a web server +3. Ensure your web server is configured for client-side routing +4. Configure HTTPS for OAuth security +5. Update OAuth redirect URIs in your GoToSocial instance if needed + +## Project Structure + +``` +src/ +├── components/ # Reusable UI components +│ ├── Layout/ # Layout components (Header, Layout) +│ ├── Compose/ # Post composition components +│ └── Post/ # Post display components +├── pages/ # Page components +│ ├── HomePage.js # Home timeline +│ ├── LoginPage.js # Authentication +│ ├── ProfilePage.js # User profiles +│ ├── SearchPage.js # Search functionality +│ └── ... +├── services/ # API and external services +│ └── api.js # GoToSocial API client +└── App.js # Main application component +``` + +## API Integration + +The project includes a comprehensive API client (`src/services/api.js`) that implements: + +- Authentication endpoints +- Timeline endpoints (home, public, local, tag) +- Status endpoints (create, delete, favorite, reblog) +- Account endpoints (profile, follow, search) +- Notification endpoints +- Search endpoints + +## Responsive Design + +The frontend is fully responsive with breakpoints for: +- **Desktop**: Full three-column layout with sidebars +- **Tablet**: Simplified layout with hidden sidebars +- **Mobile**: Single-column layout optimized for touch + +## Screenshots + +The frontend features: +- Modern gradient design with purple/blue color scheme +- Card-based layout for posts and profiles +- Intuitive navigation with clear iconography +- Responsive design that adapts to all screen sizes +- Professional typography and spacing ## Learn More diff --git a/package-lock.json b/package-lock.json index 6e25fbd..fc16216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,13 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.12.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", + "react-router-dom": "^7.9.3", "react-scripts": "5.0.1", + "styled-components": "^6.1.19", "web-vitals": "^2.1.4" } }, @@ -2366,6 +2370,27 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -3867,6 +3892,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4891,6 +4922,33 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5469,6 +5527,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -6012,6 +6079,15 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -6161,6 +6237,17 @@ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "license": "MIT" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -6369,6 +6456,12 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "license": "MIT" }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -13633,6 +13726,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -13909,6 +14008,15 @@ "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", "license": "MIT" }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -13924,6 +14032,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.9.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz", + "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz", + "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14818,6 +14973,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -14870,6 +15031,12 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15552,6 +15719,68 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -15568,6 +15797,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/package.json b/package.json index b47461b..a3c6aee 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,13 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.12.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", + "react-router-dom": "^7.9.3", "react-scripts": "5.0.1", + "styled-components": "^6.1.19", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..01f7547 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index aa069f2..9e62632 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,14 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + + + + + + + + GoToSocial React Frontend diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..df45d34 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,21 +1,11 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "GoToSocial React Frontend", + "name": "GoToSocial React Frontend", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/src/App.css b/src/App.css index 74b5e05..9e10f58 100644 --- a/src/App.css +++ b/src/App.css @@ -1,34 +1,22 @@ -.App { - text-align: center; +* { + box-sizing: border-box; } -.App-logo { - height: 40vmin; - pointer-events: none; +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { +@keyframes spin { from { transform: rotate(0deg); } @@ -36,3 +24,31 @@ transform: rotate(360deg); } } + +/* Global styles for better consistency */ +a { + text-decoration: none; + color: inherit; +} + +button { + font-family: inherit; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} diff --git a/src/App.js b/src/App.js index 3784575..fda0ecf 100644 --- a/src/App.js +++ b/src/App.js @@ -1,24 +1,161 @@ -import logo from './logo.svg'; +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import Layout from './components/Layout/Layout'; +import HomePage from './pages/HomePage'; +import PublicTimelinePage from './pages/PublicTimelinePage'; +import LocalTimelinePage from './pages/LocalTimelinePage'; +import ProfilePage from './pages/ProfilePage'; +import SearchPage from './pages/SearchPage'; +import NotificationsPage from './pages/NotificationsPage'; +import LoginPage from './pages/LoginPage'; +import StatusPage from './pages/StatusPage'; +import OAuthCallbackPage from './pages/OAuthCallbackPage'; +import api from './services/api'; +import oauthService from './services/oauth'; import './App.css'; function App() { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Check if this is an OAuth callback + const currentPath = window.location.pathname; + const hasOAuthParams = new URLSearchParams(window.location.search).has('code'); + + if (currentPath === '/oauth/callback' || hasOAuthParams) { + setIsLoading(false); // ¡IMPORTANTE! Permitir que se renderizen las rutas + return; + } + + // Check if user is already logged in + const checkAuth = async () => { + const token = localStorage.getItem('access_token'); + const instanceURL = localStorage.getItem('instance_url'); + const savedUser = localStorage.getItem('current_user'); + + if (token && instanceURL) { + try { + // Set up API client with existing credentials + api.setAuth(token, instanceURL, localStorage.getItem('refresh_token')); + + let userData; + + // Try to use saved user data first, then verify with server + if (savedUser) { + userData = JSON.parse(savedUser); + setUser(userData); + + // Verify in background and update if needed + try { + const freshUserData = await api.verifyCredentials(); + if (JSON.stringify(freshUserData) !== savedUser) { + localStorage.setItem('current_user', JSON.stringify(freshUserData)); + setUser(freshUserData); + } + } catch (error) { + // Background verification failed, but keep existing user data + } + } else { + // No saved user data, verify with server + userData = await api.verifyCredentials(); + localStorage.setItem('current_user', JSON.stringify(userData)); + setUser(userData); + } + } catch (error) { + api.clearAuth(); + } + } + setIsLoading(false); + }; + + checkAuth(); + }, []); + + const handleLoginSuccess = (userData) => { + setUser(userData); + }; + + const handleLogout = async () => { + try { + // Revoke the access token on the server + if (api.token && api.instanceUrl) { + await oauthService.revokeToken(api.token, api.instanceUrl); + } + } catch (error) { + // Token revocation failed, but continue with logout + } finally { + // Clear local auth data regardless + api.clearAuth(); + setUser(null); + } + }; + + if (isLoading) { + return ( +
+
+
+

Loading GoToSocial...

+
+
+ ); + } + return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
+ + + : + } + /> + + } + /> + + + + : + } /> + } /> + } /> + : + } /> + } /> + } /> + } /> + +

Page Not Found

+

The page you're looking for doesn't exist.

+ + } /> +
+ + } /> +
+
); } diff --git a/src/components/Compose/ComposeBox.js b/src/components/Compose/ComposeBox.js new file mode 100644 index 0000000..9047758 --- /dev/null +++ b/src/components/Compose/ComposeBox.js @@ -0,0 +1,336 @@ +import React, { useState, useRef } from 'react'; +import styled from 'styled-components'; +import { FaImage, FaSmile, FaSpinner } from 'react-icons/fa'; +import EmojiPicker from './EmojiPicker'; +import MediaPreview from './MediaPreview'; +import api from '../../services/api'; + +const ComposeContainer = styled.div` + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + margin-bottom: 2rem; +`; + +const Avatar = styled.img` + width: 48px; + height: 48px; + border-radius: 50%; + margin-right: 1rem; +`; + +const ComposeHeader = styled.div` + display: flex; + align-items: flex-start; + margin-bottom: 1rem; +`; + +const ComposeMain = styled.div` + flex: 1; +`; + +const TextArea = styled.textarea` + width: 100%; + min-height: 120px; + border: none; + resize: vertical; + font-size: 1rem; + font-family: inherit; + outline: none; + + &::placeholder { + color: #9ca3af; + } +`; + +const ComposeFooter = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; +`; + +const ComposeActions = styled.div` + display: flex; + gap: 1rem; + align-items: center; +`; + +const ActionButton = styled.button` + background: none; + border: none; + color: #6b7280; + cursor: pointer; + padding: 0.5rem; + border-radius: 6px; + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.9rem; + + &:hover { + background: #f3f4f6; + color: #374151; + } +`; + +const PrivacySelector = styled.select` + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.5rem; + background: white; + cursor: pointer; +`; + +const PostButton = styled.button` + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 20px; + padding: 0.75rem 2rem; + cursor: pointer; + font-weight: 600; + transition: transform 0.2s; + + &:hover:not(:disabled) { + transform: translateY(-2px); + } + + &:disabled { + background: #9ca3af; + cursor: not-allowed; + } +`; + +const CharacterCount = styled.div` + color: ${props => props.remaining < 20 ? '#ef4444' : '#6b7280'}; + font-size: 0.9rem; + margin-left: 1rem; +`; + +const ComposeBox = ({ user, onPost, placeholder = "What's happening?" }) => { + const [content, setContent] = useState(''); + const [privacy, setPrivacy] = useState('public'); + const [isPosting, setIsPosting] = useState(false); + const [media, setMedia] = useState([]); + const [isUploadingMedia, setIsUploadingMedia] = useState(false); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const fileInputRef = useRef(); + const textAreaRef = useRef(); + + const maxLength = 500; + const remaining = maxLength - content.length; + const maxMedia = 4; + + const handleSubmit = async () => { + if (!content.trim() || isPosting || remaining < 0) return; + + setIsPosting(true); + try { + const postData = { + status: content, + visibility: privacy + }; + + // Añadir IDs de medios si los hay + if (media.length > 0) { + postData.media_ids = media.map(m => m.id); + } + + await onPost(postData); + + // Limpiar el formulario + setContent(''); + setMedia([]); + } catch (error) { + console.error('Failed to post:', error); + } finally { + setIsPosting(false); + } + }; + + const handleFileSelect = async (event) => { + const files = Array.from(event.target.files); + if (files.length === 0) return; + + // Verificar límite de archivos + if (media.length + files.length > maxMedia) { + alert(`You can only upload up to ${maxMedia} files per post.`); + return; + } + + setIsUploadingMedia(true); + + try { + const uploadPromises = files.map(async (file) => { + // Verificar tipo de archivo + if (!file.type.startsWith('image/') && !file.type.startsWith('video/')) { + throw new Error(`Unsupported file type: ${file.type}`); + } + + // Verificar tamaño (ej: 10MB max) + const maxSize = 10 * 1024 * 1024; + if (file.size > maxSize) { + throw new Error(`File too large: ${file.name}. Maximum size is 10MB.`); + } + + const uploadedMedia = await api.uploadMedia(file); + return { + ...uploadedMedia, + file // Mantener referencia del archivo para preview + }; + }); + + const uploadedMedia = await Promise.all(uploadPromises); + setMedia(prev => [...prev, ...uploadedMedia]); + } catch (error) { + console.error('Media upload failed:', error); + alert(error.message || 'Failed to upload media. Please try again.'); + } finally { + setIsUploadingMedia(false); + // Limpiar el input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + const handleRemoveMedia = (mediaId) => { + setMedia(prev => prev.filter(m => m.id !== mediaId)); + }; + + const handleUpdateMediaAlt = async (mediaId, description) => { + try { + await api.updateMedia(mediaId, description); + setMedia(prev => prev.map(m => + m.id === mediaId ? { ...m, description } : m + )); + } catch (error) { + console.error('Failed to update media description:', error); + alert('Failed to update alt text. Please try again.'); + } + }; + + const handleEmojiSelect = (emoji) => { + const textarea = textAreaRef.current; + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const newContent = content.slice(0, start) + emoji + content.slice(end); + + setContent(newContent); + + // Restaurar posición del cursor + setTimeout(() => { + const newCursorPos = start + emoji.length; + textarea.setSelectionRange(newCursorPos, newCursorPos); + textarea.focus(); + }, 0); + }; + + const handleImageButtonClick = () => { + if (isUploadingMedia) return; + fileInputRef.current?.click(); + }; + + const handleEmojiButtonClick = () => { + setShowEmojiPicker(!showEmojiPicker); + }; + + return ( + + + {user?.avatar && ( + + )} + +