diff --git a/.github/ISSUE_TEMPLATE/issues_template.md b/.github/ISSUE_TEMPLATE/issues_template.md new file mode 100644 index 0000000..b89f3bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issues_template.md @@ -0,0 +1,55 @@ + +# Akeru.ai Issues Standard Template + +**Title**: [Clear, concise title describing the issue] + +**Description**: +Detail the problem or enhancement proposed. Explain the background and why it's important to address this issue. + +**Objectives**: + +- List specific goals or outcomes expected from resolving this issue. +- Detail the sections of the project affected and suggest possible solutions if applicable. + +**Expected Outcomes**: + +- Describe what a successful resolution will look like. +- Mention how it will improve the project (e.g., enhanced usability, reduced errors). + +**Tags**: + +- Include relevant tags to help categorize the issue (e.g., documentation, enhancement, bug). + +**Assignees**: + +- Tag potential contributors who might be best suited to tackle this issue. + +**Example**: + +## Title: Enhance README Documentation for Akeru.ai + +### Description + +The README file is the first point of interaction for potential users and contributors to Akeru.ai. As the project grows and incorporates more features, it's crucial that the README accurately reflects the capabilities and mission of Akeru.ai, providing clear and engaging content that facilitates understanding and involvement. + +### Objectives + +- **Expand Introduction**: Clearly define what Akeru.ai does, the problems it solves, and how it's distinct from other AI platforms. +- **Detail API Features**: Include descriptions of current and upcoming features, expected use cases, and any associated limitations or costs. +- **Clarify Bittensor Subnet Integration**: Explain how the subnet supports the platform, including any security features and the advantages of decentralization. +- **Update Self-Hosting and Validation Sections**: Provide preliminary guidelines and expectations for users interested in self-hosting or participating as validators/miners. +- **Visual Enhancements**: Propose the creation of diagrams and workflows to help visually explain complex concepts. + +### Expected Outcomes + +- A comprehensive, well-structured README that serves as an effective entry point for engaging new users and contributors. +- Enhanced clarity on technical aspects and project roadmap, facilitating easier onboarding and collaboration. + +### Tags + +- `documentation`, `enhancement` + +### Assignees + +- @username (Documentation Lead) +- @username2 (Technical Reviewer) \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 72d6718..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,32 +0,0 @@ - - -## Description of the pull request - - - - -## Changes made - - - -- Change 1 -- Change 2 -- .... - -## Related issues - - - -## Testings done - - - -## Screenshots (if any) - - - -## Checklist - -- [ ] I have written tests -- [ ] My code does not produce new errors -- [ ] I gave myself a code review before asking others. diff --git a/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bba5b96 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,51 @@ +# Akeru.ai Pull Request Standard Template + +**Title**: [Descriptive title reflecting the main change] + +**Description**: +Briefly describe the purpose of the pull request and what it aims to achieve. Include key changes made, such as feature additions, bug fixes, or enhancements. + +**Key Changes**: + +- List significant changes and their impact on the project. +- Mention any particular areas of the codebase affected. + +**Link to Issues**: + +- Reference related issue numbers that this pull request addresses. + +**Request for Review**: + +- Tag reviewers who are familiar with the impacted areas of the codebase. + +## Checklist + +- [ ] I have written tests +- [ ] My code does not produce new errors +- [ ] I gave myself a code review before asking others. + +## An Example + +## Title: Update README for Enhanced Project Clarity and Information + +### Description + +This pull request updates the README to provide a clearer and more comprehensive understanding of Akeru.ai. The enhancements include detailed descriptions of new API features, the Bittensor Subnet integration, and improved section formatting. These updates aim to make the repository more welcoming and informative for new contributors and users. + +### Key Changes + +- **Expanded Introduction**: Better explains Akeru.ai's unique features and its differentiation in the AI space. +- **Detailed API Features**: Includes use cases and development status for upcoming features. +- **Clarified Bittensor Subnet Integration**: Details on how the Bittensor Subnet enhances Akeru.ai’s capabilities, including security features and decentralized benefits. +- **Preliminary Self-Hosting Info**: Provides initial guidelines on self-hosting and the requirements for validators and miners. + +### Link to Issues + +- Addresses feedback from issue #[issue_number] for more detailed documentation. + +### Request for Review + +- @username1 +- @username2 + +Looking forward to your feedback and suggestions to further refine our project documentation. diff --git a/.gitignore b/.gitignore index a20b6a9..50eb9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -180,4 +180,5 @@ dist .env.development .env.test -.DS_Store \ No newline at end of file +.DS_Store +package-lock.json diff --git a/README.md b/README.md index f979004..b51874c 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ We welcome contributions of all forms, from code to documentation. Here’s how - **Get Involved**: Check out our issues tab on GitHub. - **Submit Pull Requests**: Contributions are reviewed regularly—ensure your code adheres to our standards. -- **See full Contributing guide [here](#).** +- **See full Contributing guide [here](./CONTRIBUTING.md).** ## Visuals and Diagrams diff --git a/bun.lockb b/bun.lockb index 3c2e391..9982c45 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md new file mode 100644 index 0000000..2a0920b --- /dev/null +++ b/docs/code_of_conduct.md @@ -0,0 +1,135 @@ + +# Akeru.ai Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the [Akeru.ai](https://www.akeru.ai) community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at guibibeau@gmail.com. + +**All complaints will be reviewed and investigated promptly and fairly.** + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. + +Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/coding_conventions.md b/docs/coding_conventions.md new file mode 100644 index 0000000..2227aec --- /dev/null +++ b/docs/coding_conventions.md @@ -0,0 +1,77 @@ +# Akeru.ai Coding Conventions + +## Introduction + +This document outlines the coding standards and best practices for the Akeru.ai project. Adhering to these conventions is essential to maintain code quality, facilitate collaboration, and ensure the project is scalable and maintainable. + +## Table of Contents + +- [General Principles](#general-principles) +- [Language-Specific Standards](#language-specific-standards) +- [Formatting and Style](#formatting-and-style) +- [Commenting and Documentation](#commenting-and-documentation) +- [Error Handling](#error-handling) +- [Security Practices](#security-practices) +- [Performance Optimization](#performance-optimization) +- [Version Control Practices](#version-control-practices) +- [Testing Standards](#testing-standards) +- [Build and Deployment](#build-and-deployment) + +## General Principles + +- **Readability and Clarity**: Code should be written as if the next person to read it is a serial killer who knows where you live. Prioritize clarity. +- **Maintainability**: Write code that is easy to maintain and extend. Any developer should be able to understand your code and make changes when necessary. +- **DRY Principle**: Don't Repeat Yourself. Ensure that you don't have duplicate code scattered throughout the codebase. + +## Language-Specific Standards + +### JavaScript/TypeScript + +- Use [ESLint with a configuration based on Next.js](https://nextjs.org/docs/pages/building-your-application/configuring/eslint) standards. +- Follow [TypeScript strict typing](https://www.typescriptlang.org/tsconfig#strict) as much as possible for type safety. + +### Python (if applicable) + +- Follow [PEP 8 standards](https://peps.python.org/pep-0008/) for Python code. +- Use type hints for better maintainability. + +## Formatting and Style + +- **Indentation**: Use spaces (not tabs) and set the width to 2 or 4 spaces per indent level, depending on the language. +- **Braces**: Use the "1TBS (One True Brace Style)" where braces open on the same line as the statement but close on a new line. +- **Variable Naming**: Use `camelCase` for identifiers in JavaScript and `snake_case` for Python variables. + +## Commenting and Documentation + +- **Code Comments**: Write comments that explain "why" something is done, not "what" is done. The code itself should explain "what." +- **Documentation**: Use [JSDoc](https://jsdoc.app/) for JavaScript and [Docstrings](https://peps.python.org/pep-0257/) for Python. Document all public APIs and critical internal mechanisms. + +## Error Handling + +- **Consistency**: Use a consistent method across the entire codebase to handle errors. In Node.js, use asynchronous error handling with promises and async/await. +- **Logging**: Implement comprehensive logging for errors. Use a library that supports different log levels (e.g., debug, info, error). + +## Security Practices + +- **Input Validation**: Always validate external inputs to avoid [SQL injections](https://www.simplilearn.com/tutorials/cyber-security-tutorial/what-is-sql-injection) and other malicious attacks. +- **Dependency Management**: Regularly update dependencies to mitigate vulnerabilities, using tools like [Dependabot](https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide). + +## Performance Optimization + +- **Best Practices**: Follow language-specific performance optimization techniques, such as optimizing loops and asynchronous programming in JavaScript. +- **Profiling**: Regularly profile the application and optimize bottlenecks. + +## Version Control Practices + +- **Branching**: Follow a branching model like [Git Flow](https://github.com/nvie/gitflow) to manage features, fixes, and releases. +- **Commit Messages**: Write clear, concise commit messages that explain the why and what of the changes. + +## Testing Standards + +- **Coverage**: Aim for a high test coverage percentage. Use tools like [Istanbul](https://istanbul.js.org/) for JavaScript to check coverage. +- **Frameworks**: Use [Jest](https://jestjs.io/) for JavaScript testing. Ensure tests are thorough and cover expected and unexpected use cases. + +## Build and Deployment + +- **Automated Builds**: Use CI/CD pipelines to automate builds and deployments. Tools like [Jenkins](https://www.jenkins.io/) or [GitHub Actions](https://docs.github.com/en/actions) can be integrated to manage these processes. +- **Environment Specifics**: Ensure configurations such as API keys and endpoints are environment-specific and secured. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..77d0885 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,93 @@ +# Contributing to Akeru.ai + +Welcome to the Akeru.ai community! We are thrilled to have you onboard. This document is designed to ensure that your contributions to our open-source AI platform are as smooth and effective as possible. + +## Table of Contents + +1. [Legal and Licensing](#legal-and-licensing) +2. [Project Overview and Code of Conduct](#project-overview-and-code-of-conduct) +3. [Getting Started](#getting-started) +4. [Contribution Process](#contribution-process) +5. [Coding Conventions](#coding-conventions) +6. [Building and Testing](#building-and-testing) +7. [Documentation Standards](#documentation-standards) +8. [Handling Issues and Pull Requests](#handling-issues-and-pull-requests) +9. [Community and Communication](#community-and-communication) +10. [Recognition and Rewards](#recognition-and-rewards) +11. [Accessibility Guidelines](#accessibility-guidelines) +12. [Internationalization](#internationalization) +13. [Security Practices](#security-practices) + +### Legal and Licensing + +[Akeru.ai](https://www.akeru.ai) is released under the MIT License. It is important to understand how this affects your contributions. Read the full license [here](https://opensource.org/licenses/MIT). + +### Project Overview and Code of Conduct + +[Akeru.ai](https://www.akeru.ai) leverages the power of the Akeru AI edge network running on a Bittensor Subnet to offer transparent, safe, and highly available AI capabilities. We strive to maintain a welcoming and inclusive community. + +Our [Code of Conduct](/docs/code_of_conduct.md) outlines expected behavior and practices to ensure a collaborative and respectful environment. + +### Getting Started + +Setting up your development environment is the first step in contributing: + +- **Subnet Setup Guide**: For working with the subnet, follow the instructions [here](https://github.com/GuiBibeau/akeru/blob/main/subnet/validator/README.md). + +- **Platform Setup Guide**: Additional setup instructions for the platform will be provided soon. + +### Contribution Process + +To contribute to [Akeru.ai](https://www.akeru.ai): + +1. **Fork the repository** on GitHub. +2. **Clone your fork** to your local machine (`git clone url`). +3. **Create a branch** for your changes (`git checkout -b feature-branch-name`). +4. **Make your changes** and commit them (`git commit -am 'Add some feature'`). +5. **Push your branch** to your fork (`git push origin feature-branch-name`). +6. **Create a pull request** against our main branch. + +### Coding Conventions + +[Akeru.ai](https://www.akeru.ai) follows Next.js linting standards in the Next app and plans to setup ESLint for the API and service mesh parts. For coding styles, we use Prettier. Our Prettier config will serve as the source of truth. View the full coding convention [here](/docs/coding_conventions.md). + +### Building and Testing + +- **Website**: Automated by Vercel. +- **API**: Currently under development for automated processes. +- **Subnet**: Managed externally; details to be provided later. + +### Documentation Standards + +All contributions should include relevant documentation updates: + +- **Formatting Guidelines**: Follow our documentation style guide [here](/docs/documentation_style_guide.md). +- **Where to Add Documentation**: Add to `/docs` or inline as appropriate. + +### Handling Issues and Pull Requests + +- **Reporting Issues**: Use our template [here](/docs/issue_and_pr_template.md). Ensure to include the detailed steps to reproduce the issue. +- **Pull Requests**: Use our PR template, link related issues, and provide a detailed description of your changes. Check that out [here](/PULL_REQUEST_DOC_TEMPLATE.md) + +### Community and Communication + +Join the conversation on [Discord] and stay updated: + +- **Main Channels**: [Links to communication platforms] +- **Communication Norms**: Engage respectfully and frequently to collaborate effectively. + +### Recognition and Rewards + +Details on contributor recognition and rewards are currently being developed. + +### Accessibility Guidelines + +We are committed to making our project accessible and plan to enhance this by adding specific linting rules. Current Next.js accessibility rules are a good baseline. + +### Internationalization + +Plans are in development and will be documented accordingly. + +### Security Practices + +Detailed security protocols, especially for the subnet, are currently under development and will be added upon completion. diff --git a/docs/documentation_style_guide.md b/docs/documentation_style_guide.md new file mode 100644 index 0000000..8d997ba --- /dev/null +++ b/docs/documentation_style_guide.md @@ -0,0 +1,68 @@ +# Akeru.ai Documentation Style Guide + +## Introduction + +This guide establishes standards for creating clear, accessible, and effective documentation for the Akeru.ai project. It ensures consistency and quality across all documentation to aid both contributors and end-users. + +## Table of Contents + +1. [Purpose and Audience](#purpose-and-audience) +2. [Tone and Style](#tone-and-style) +3. [Document Structure](#document-structure) +4. [Writing Guidelines](#writing-guidelines) +5. [Formatting](#formatting) +6. [Language and Grammar](#language-and-grammar) +7. [Technical Content](#technical-content) +8. [Visual Content](#visual-content) +9. [Review Process](#review-process) +10. [Feedback and Updates](#feedback-and-updates) + +### Purpose and Audience + +- **Purpose**: Define the purpose of each document type, whether for API usage, setup instructions, or end-user operations. +- **Audience**: Identify the primary and secondary audiences, and tailor content to their technical level and needs. + +### Tone and Style + +- **Tone**: Use a professional, engaging tone. Opt for the active voice and present tense to keep the documentation lively and readable. +- **Inclusivity**: Ensure language is inclusive and accessible, avoiding jargon and providing explanations for technical terms. + +### Document Structure + +- **Headers**: Use headers to logically organize information. Begin with an introduction, follow with detailed content, and conclude with a summary or call to action. +- **Navigation**: Include a clickable table of contents in longer documents for easy navigation. + +### Writing Guidelines + +- **Clarity and Brevity**: Aim for concise explanations. Avoid redundant words and focus on clear, direct communication. +- **Consistency**: Use consistent terminology throughout your documentation to avoid confusion. + +### Formatting + +- **Highlighting Techniques**: Utilize **bold** for key terms, *italics* for emphasis, and `code` for inline code references. +- **Lists and Bullet Points**: Use bullet points for unordered lists and numbered lists for sequences or instructions. + +### Language and Grammar + +- **Grammar and Spelling**: Utilize tools like [Grammarly](https://www.grammarly.com/) and [Hemingway Editor](http://www.hemingwayapp.com/) to ensure correctness. Adhere to American English spelling conventions. +- **Examples and Tutorials**: Provide clear examples and detailed tutorials. Use [link to a style guide](/docs/documentation_style_guide.md) for complex topics. + +### Technical Content + +- **Code Examples**: Include fully tested code examples to illustrate technical explanations. Ensure they adhere to the project's coding conventions. +- **API Documentation**: Clearly detail parameters, methods, and example requests and responses for API calls. + +### Visual Content + +- **Screenshots and Diagrams**: Ensure screenshots are current and clearly annotated. Use diagrams to explain complex processes; tools like [Lucidchart](https://www.lucidchart.com/) or [Draw.io](https://draw.io/) can be helpful. + +### Review Process + +- **Peer Review**: Documentation should undergo peer review to catch errors and ensure it meets quality standards. +- **Iterative Reviews**: Regularly scheduled reviews of documentation to update and refine content. + +### Feedback and Updates + +- **Feedback Mechanism**: Implement a system for collecting feedback on documentation, such as a feedback form linked at the end of each document. +- **Regular Updates**: Establish a routine for regularly updating documents to reflect changes in the software and feedback from users. + diff --git a/docs/issue_and_pr_template.md b/docs/issue_and_pr_template.md new file mode 100644 index 0000000..ac05e44 --- /dev/null +++ b/docs/issue_and_pr_template.md @@ -0,0 +1,110 @@ +# Akeru.ai Issue and Pull Request Templates + +## Table of Contents + +1. [Issue Template](#akeruai-issue-template) +2. [Pull Request Template](#akeruai-pull-request-template) + +## Akeru.ai Issue Template + +**Title**: [Enter a clear, concise title describing the issue] + +### Description + +Provide a detailed description of the problem or the enhancement proposed. Explain the background and the importance of addressing this issue. + +### Objectives + +- List the specific goals or outcomes expected from resolving this issue. +- Detail the sections of the project affected and suggest possible solutions, if applicable. + +### Expected Outcomes + +- Describe what a successful resolution will look like. +- Explain how it will improve the project (e.g., enhanced usability, reduced errors). + +### Tags + +- Include relevant tags to help categorize the issue (e.g., `documentation`, `enhancement`, `bug`). + +### Assignees + +- Tag potential contributors who might be best suited to tackle this issue. + +### Example + +#### Title: Enhance README Documentation for Akeru.ai + +#### Description + +The README file is the first point of interaction for potential users and contributors to Akeru.ai. As the project grows and incorporates more features, it's crucial that the README accurately reflects the capabilities and mission of Akeru.ai, providing clear and engaging content that facilitates understanding and involvement. + +#### Objectives + +- Expand Introduction: Clearly define what Akeru.ai does, the problems it solves, and how it's distinct from other AI platforms. +- Detail API Features: Include descriptions of current and upcoming features, expected use cases, and any associated limitations or costs. +- Clarify Bittensor Subnet Integration: Explain how the subnet supports the platform, including any security features and the advantages of decentralization. +- Update Self-Hosting and Validation Sections: Provide preliminary guidelines and expectations for users interested in self-hosting or participating as validators/miners. +- Visual Enhancements: Propose the creation of diagrams and workflows to help visually explain complex concepts. + +#### Expected Outcomes + +- A comprehensive, well-structured README that serves as an effective entry point for engaging new users and contributors. +- Enhanced clarity on technical aspects and project roadmap, facilitating easier onboarding and collaboration. + +#### Tags + +`documentation`, `enhancement` + +#### Assignees + +- @username (Documentation Lead) +- @username2 (Technical Reviewer) + +## Akeru.ai Pull Request Template +**Title**: [Descriptive title reflecting the main change] + +### Description + +Briefly describe the purpose of the pull request and what it aims to achieve. Include key changes made, such as feature additions, bug fixes, or enhancements. + +### Key Changes + +- List significant changes and their impact on the project. +- Mention any particular areas of the codebase affected. + +### Link to Issues + +- Reference related issue numbers that this pull request addresses. + +### Request for Review + +- Tag reviewers who are familiar with the impacted areas of the codebase. + +### Checklist +- [ ] I have written tests. +- [ ] My code does not produce new errors. +- [ ] I gave myself a code review before asking others. + +### Example + +#### Title: Update README for Enhanced Project Clarity and Information +#### Description +This pull request updates the README to provide a clearer and more comprehensive understanding of Akeru.ai. The enhancements include detailed descriptions of new API features, the Bittensor Subnet integration, and improved section formatting. These updates aim to make the repository more welcoming and informative for new contributors and users. + +#### Key Changes + +- Expanded Introduction: Better explains Akeru.ai's unique features and its differentiation in the AI space. +- Detailed API Features: Includes use cases and development status for upcoming features. +- Clarified Bittensor Subnet Integration: Details on how the Bittensor Subnet enhances Akeru.ai’s capabilities, including security features and decentralized benefits. +- Preliminary Self-Hosting Info: Provides initial guidelines on self-hosting and the requirements for validators and miners. + +#### Link to Issues + +- Addresses feedback from issue #[issue_number] for more detailed documentation. + +#### Request for Review +- @username1 +- @username2 + +Looking forward to your feedback and suggestions to further refine our project documentation. diff --git a/packages/akeru-frontend/README.md b/packages/akeru-frontend/README.md index c403366..a95df2e 100644 --- a/packages/akeru-frontend/README.md +++ b/packages/akeru-frontend/README.md @@ -1,5 +1,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +## Important Note + +This codebase at the moment is being deprecated due to certain reasons; we have decided to adopt a different setup which is named `studios-ts` for now; that matches the goals of this project. + ## Getting Started First, run the development server: diff --git a/packages/akeru-frontend/app/page.tsx b/packages/akeru-frontend/app/page.tsx index 8849402..765b9cc 100644 --- a/packages/akeru-frontend/app/page.tsx +++ b/packages/akeru-frontend/app/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import HomeCard from "./components/ui/home-card/home-card"; import HeroSection from "./components/sections/hero-section"; import FormSection from "./components/sections/form-section"; +import { homeCardData } from "./utils/data"; export const metadata = { title: "AkeruAI - HOME", @@ -13,21 +14,13 @@ export default function Home() {
+
- - - + {homeCardData.map((card) => ( + + ))}
); diff --git a/packages/akeru-frontend/app/utils/data.ts b/packages/akeru-frontend/app/utils/data.ts new file mode 100644 index 0000000..fbb65e2 --- /dev/null +++ b/packages/akeru-frontend/app/utils/data.ts @@ -0,0 +1,17 @@ +export const homeCardData = [ + { + title: "Decentralized AI Powerhouse", + description: + "Explore AI's potential with Akeru.ai's decentralized platform, ensuring transparency and reliability through its edge network.", + }, + { + title: "Seamless Integration, Superior UX", + description: + "Elevate your apps effortlessly with Akeru.ai's API, designed for easy integration and exceptional user experience.", + }, + { + title: "Community-Driven Innovation", + description: + "Join a vibrant community shaping the future of AI. Contribute, collaborate, and innovate with Akeru.ai's open-source platform. ", + }, +]; diff --git a/packages/akeru-frontend/app/utils/header-routes.tsx b/packages/akeru-frontend/app/utils/header-routes.tsx index 110c2c7..1ac3598 100644 --- a/packages/akeru-frontend/app/utils/header-routes.tsx +++ b/packages/akeru-frontend/app/utils/header-routes.tsx @@ -1,14 +1,14 @@ export const headerRoutes = [ - { - text: "Docs", - href: "#1" - }, - { - text: "Examples", - href: "#2", - }, - { - text: "About", - href: "#3" - }, -] \ No newline at end of file + { + text: "Docs", + href: "https://github.com/AkeruAI/akeru/blob/main/README.md", + }, + { + text: "Examples", + href: "#2", + }, + { + text: "About", + href: "#3", + }, +]; diff --git a/packages/akeru-server/package.json b/packages/akeru-server/package.json index 8323dde..5a9a5ba 100644 --- a/packages/akeru-server/package.json +++ b/packages/akeru-server/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@elysiajs/bearer": "^1.0.2", + "@elysiajs/stream": "^1.0.2", "@elysiajs/swagger": "^1.0.4", "dayjs": "^1.11.10", "elysia": "latest", diff --git a/packages/akeru-server/scripts/cleanDB.ts b/packages/akeru-server/scripts/cleanDB.ts index 52ec317..0636f0e 100644 --- a/packages/akeru-server/scripts/cleanDB.ts +++ b/packages/akeru-server/scripts/cleanDB.ts @@ -1,4 +1,4 @@ -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; const script = async () => { try { diff --git a/packages/akeru-server/scripts/createSuperAdmin.ts b/packages/akeru-server/scripts/createSuperAdmin.ts index 555057a..fe422db 100644 --- a/packages/akeru-server/scripts/createSuperAdmin.ts +++ b/packages/akeru-server/scripts/createSuperAdmin.ts @@ -5,7 +5,6 @@ import { async function createSuperAdminWithApiKey() { const userData = { - // Add any necessary user data here }; const userId = await createSuperAdmin(userData); @@ -15,6 +14,7 @@ async function createSuperAdminWithApiKey() { } const apiKey = await createApiToken(userId); + if (!apiKey) { console.error("Failed to create API key for super admin user"); return; diff --git a/packages/akeru-server/src/core/application/controllers/assistant/assistantController.test.ts b/packages/akeru-server/src/core/application/controllers/assistant/assistantController.test.ts index 6d34080..c8f2aea 100644 --- a/packages/akeru-server/src/core/application/controllers/assistant/assistantController.test.ts +++ b/packages/akeru-server/src/core/application/controllers/assistant/assistantController.test.ts @@ -1,6 +1,6 @@ import { createSuperAdminForTesting } from "@/__tests__/utils"; import { app } from "@/index"; -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; import { test, expect, describe } from "bun:test"; import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues"; diff --git a/packages/akeru-server/src/core/application/controllers/userController.ts b/packages/akeru-server/src/core/application/controllers/userController.ts index 88373b9..c8ae55f 100644 --- a/packages/akeru-server/src/core/application/controllers/userController.ts +++ b/packages/akeru-server/src/core/application/controllers/userController.ts @@ -1,6 +1,6 @@ import { assignRole, createUser } from "core/application/services/userService"; import { createToken } from "@/core/application/services/tokenService"; -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; import { ulid } from "ulid"; /** diff --git a/packages/akeru-server/src/core/application/services/assistantService.ts b/packages/akeru-server/src/core/application/services/assistantService.ts index 3ea6e3d..a6f0969 100644 --- a/packages/akeru-server/src/core/application/services/assistantService.ts +++ b/packages/akeru-server/src/core/application/services/assistantService.ts @@ -1,5 +1,5 @@ import type { Assistant } from "@/core/domain/assistant"; -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; /** * Creates an assistant in Redis if it does not exist and adds a 'CREATED_BY' relationship to the user. diff --git a/packages/akeru-server/src/core/application/services/messageService.ts b/packages/akeru-server/src/core/application/services/messageService.ts index 176d260..b74a724 100644 --- a/packages/akeru-server/src/core/application/services/messageService.ts +++ b/packages/akeru-server/src/core/application/services/messageService.ts @@ -1,8 +1,8 @@ import { v4 as uuidv4 } from "uuid"; -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; import { Message } from "@/core/domain/messages"; import dayjs from "dayjs"; import { getUserRole } from "./userService"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; export async function createMessage( userId: string, diff --git a/packages/akeru-server/src/core/application/services/runService.ts b/packages/akeru-server/src/core/application/services/runService.ts index 0baff4b..93913c8 100644 --- a/packages/akeru-server/src/core/application/services/runService.ts +++ b/packages/akeru-server/src/core/application/services/runService.ts @@ -4,8 +4,10 @@ import { ThreadRun, ThreadRunRequest } from "@/core/domain/run"; import { getAssistantData } from "./assistantService"; import { getThread } from "./threadService"; import { createMessage, getAllMessage } from "./messageService"; -import { gpt4Adapter } from "@/infrastructure/adaptaters/openai/gpt4Adapter"; + import { Role } from "@/core/domain/roles"; +import { AdapterManager } from "@/infrastructure/adapters/AdapterManager"; + import { v4 as uuidv4 } from "uuid"; export async function runAssistantWithThread(runData: ThreadRunRequest) { @@ -17,7 +19,8 @@ export async function runAssistantWithThread(runData: ThreadRunRequest) { ]); // If no thread data or assistant data, an error should be thrown as we need both to run a thread - if (!threadData || !assistantData) throw new Error("No thread or assistant found."); + if (!threadData || !assistantData) + throw new Error("No thread or assistant found."); const everyMessage = await getAllMessage(threadData.id); // only get role and content from every message for context. @@ -30,25 +33,28 @@ export async function runAssistantWithThread(runData: ThreadRunRequest) { }; }); - // Calls the appropriate adapter based on what model the assistant uses - if (assistantData.model === "gpt-4") { - const gpt4AdapterRes: any = await gpt4Adapter( - everyRoleAndContent, - assistantData.instruction - ); - - const assistantResponse: string = gpt4AdapterRes.choices[0].message.content; - - // add assistant response to the thread - await createMessage(assistant_id, thread_id, assistantResponse); - - const threadRunResponse: ThreadRun = { - id: uuidv4(), - assistant_id: assistant_id, - thread_id: thread_id, - created_at: new Date(), - }; + const adapter = AdapterManager.instance.getBaseAdapter(assistantData.model); - return threadRunResponse; + if (!adapter) { + throw new Error("Adapter not found"); } + + // Generate response using the appropriate adapter + const assistantResponse: string = await adapter.generateSingleResponse({ + message_content: everyRoleAndContent, + instruction: assistantData.instruction, + }); + + // Add assistant response to the thread + await createMessage(assistant_id, thread_id, assistantResponse); + + // Return the thread run response + const threadRunResponse: ThreadRun = { + assistant_id: assistant_id, + thread_id: thread_id, + id: uuidv4(), + created_at: new Date(), + }; + + return threadRunResponse; } diff --git a/packages/akeru-server/src/core/application/services/threadService.ts b/packages/akeru-server/src/core/application/services/threadService.ts index f4ef5e6..dbe3c2a 100644 --- a/packages/akeru-server/src/core/application/services/threadService.ts +++ b/packages/akeru-server/src/core/application/services/threadService.ts @@ -1,5 +1,5 @@ import { Thread } from "@/core/domain/thread"; -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; /** * Creates a new thread in Redis. diff --git a/packages/akeru-server/src/core/application/services/tokenService.ts b/packages/akeru-server/src/core/application/services/tokenService.ts index 57a017b..7b9e804 100644 --- a/packages/akeru-server/src/core/application/services/tokenService.ts +++ b/packages/akeru-server/src/core/application/services/tokenService.ts @@ -1,4 +1,4 @@ -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; import { getUserPermissions } from "@/core/application/services/userService"; import jwt from "jsonwebtoken"; import { PermissionDetailArray } from "@/core/domain/permissions"; diff --git a/packages/akeru-server/src/core/application/services/userService.ts b/packages/akeru-server/src/core/application/services/userService.ts index 399628b..8f1247c 100644 --- a/packages/akeru-server/src/core/application/services/userService.ts +++ b/packages/akeru-server/src/core/application/services/userService.ts @@ -2,7 +2,7 @@ import { Role, getRolePermissions } from "@/core/domain/roles"; import { HumanUserBody, User, UserRole } from "@/core/domain/user"; -import { redis } from "@/infrastructure/adaptaters/redisAdapter"; +import { redis } from "@/infrastructure/adapters/redisAdapter"; import { ulid } from "ulid"; /** diff --git a/packages/akeru-server/src/core/domain/assistant.ts b/packages/akeru-server/src/core/domain/assistant.ts index de6434b..283d001 100644 --- a/packages/akeru-server/src/core/domain/assistant.ts +++ b/packages/akeru-server/src/core/domain/assistant.ts @@ -1,8 +1,10 @@ +import { SupportedModels } from "@/infrastructure/adapters/adapter"; + export type Assistant = { id: string; name: string; - model: "gpt-4"; + model: SupportedModels; tools: { type: string }[]; fileIds: string[]; - instruction: string + instruction: string; }; diff --git a/packages/akeru-server/src/core/domain/run.ts b/packages/akeru-server/src/core/domain/run.ts index 7c5a063..5bf597a 100644 --- a/packages/akeru-server/src/core/domain/run.ts +++ b/packages/akeru-server/src/core/domain/run.ts @@ -9,6 +9,7 @@ export interface ThreadRun { created_at: Date; assistant_id: Assistant["id"]; thread_id: Thread["id"]; + stream?: boolean } -export type ThreadRunRequest = Pick +export type ThreadRunRequest = Pick; diff --git a/packages/akeru-server/src/infrastructure/adaptaters/openai/gpt4Adapter.ts b/packages/akeru-server/src/infrastructure/adaptaters/openai/gpt4Adapter.ts deleted file mode 100644 index 9cbc0cf..0000000 --- a/packages/akeru-server/src/infrastructure/adaptaters/openai/gpt4Adapter.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Message } from "@/core/domain/messages"; - -interface ChatMessage { - content: string, - role: "assistant" | "system" | "user" -} - -interface ChatResponseChoice{ - finish_reason: string, - index: number, - message: ChatMessage -} - -export interface OpenAIResponse { - choices: ChatResponseChoice[] -} - -export async function gpt4Adapter( - messages: Pick[], - assistant_instructions: string -): Promise { - // System will always be the assistant_instruction that created the assistant - const gpt_messages = [{role: "system", content: assistant_instructions}].concat(messages) - try { - const res = await fetch("https://api.openai.com/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, - }, - body: JSON.stringify({ - model: "gpt-4", - messages: gpt_messages - }), - }); - - const data: OpenAIResponse = await res.json(); - return data; - } catch (error) { - return new Response("Error", { status: 500 }); - } -} \ No newline at end of file diff --git a/packages/akeru-server/src/infrastructure/adapters/AdapterManager.ts b/packages/akeru-server/src/infrastructure/adapters/AdapterManager.ts new file mode 100644 index 0000000..f9b89fe --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/AdapterManager.ts @@ -0,0 +1,49 @@ +import { BaseAdapter, StreamableAdapter } from "./BaseAdapter"; +import { SupportedModels } from "./adapter"; +import { GPTAdapter } from "./openai/GPTAdapter"; +import { GPTModels } from "./openai/models"; + +/** + * The AdapterManager is used to decide what adapter to use for a given assistant + * @param StreamableAdapter - The adapter that is used to generate a streamable response + * @param Adapters - The adapter that is used to generate a single response + * @returns The AdapterManager instance + */ +export class AdapterManager { + + private StreamableAdapter: Map = new Map(); + private Adapters: Map = new Map(); + public static instance: AdapterManager = new AdapterManager(); + + constructor(){ + this.initStreamableAdapter(); + this.initAdapters(); + } + + /** + * Initialize the StreamableAdapter + * Sets all the streamable adapters that are available + */ + private initStreamableAdapter() { + this.StreamableAdapter.set("gpt-4", new GPTAdapter("gpt-4")); + this.StreamableAdapter.set("gpt-3.5-turbo", new GPTAdapter("gpt-3.5-turbo")); + this.StreamableAdapter.set("gpt-4-turbo-preview", new GPTAdapter("gpt-4-turbo-preview")); + } + + /** + * Initialize the Adapters + * Sets all the adapters that are available, that does not support streamable responses + */ + private initAdapters() { + this.Adapters.set("gpt-4", new GPTAdapter("gpt-4")); + this.Adapters.set("gpt-3.5-turbo", new GPTAdapter("gpt-3.5-turbo")); + this.Adapters.set("gpt-4-turbo-preview", new GPTAdapter("gpt-4-turbo-preview")); + } + + public getStreamableAdapter(adapterName: SupportedModels): StreamableAdapter | undefined { + return this.StreamableAdapter.get(adapterName); + } + public getBaseAdapter(adapterName: SupportedModels): BaseAdapter | undefined { + return this.Adapters.get(adapterName); + } +} diff --git a/packages/akeru-server/src/infrastructure/adapters/BaseAdapter.ts b/packages/akeru-server/src/infrastructure/adapters/BaseAdapter.ts new file mode 100644 index 0000000..13e09ca --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/BaseAdapter.ts @@ -0,0 +1,28 @@ +import { AdapterRequest } from "./adapter"; + +export interface StreamableAdapter { + generateStreamableResponse(args: AdapterRequest): Promise; +} + +/** + * BaseAdapter defines the interface for any Adapters that we have without defining the implementation. + */ +export abstract class BaseAdapter { + + abstract adapterName: string; + abstract adapterDescription: string; + + /** + * Based on a set of roles and content, generate a response. + * @param everyRoleAndContent - An array of roles and content. + * @param instruction - The instruction given to the assistant. + */ + abstract generateSingleResponse(args: AdapterRequest): Promise; + + /** + * Based on the attributes of the adapter return the response of the adapter. + * This implementation can vary from adapters, for instance OpenAI adapters have their own endpoints that can be called. + * @returns Any object that contains the information of the adapter. + */ + abstract getAdapterInformation(): Object; +} diff --git a/packages/akeru-server/src/infrastructure/adapters/adapter.ts b/packages/akeru-server/src/infrastructure/adapters/adapter.ts new file mode 100644 index 0000000..f2a3915 --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/adapter.ts @@ -0,0 +1,23 @@ +import { Message } from "@/core/domain/messages" +import { GPTModels } from "./openai/models" +import { ValidatorModels } from "./validators/models" + +/** + * AdapterMessageContent will be part of the prompt for the ai adapter to generate a response + * @param content: string The content of the message + * @param role: Role This role is stringified + */ +export type AdapterMessageContent = Pick + +/** + * AdapterRequest is the prompt to the adapter to generate a response + * @param message_content: AdapterMessageContent The content of the message + * @param instruction: string The instruction for the adapter to follow based on the selected assistant + */ +export interface AdapterRequest { + message_content: AdapterMessageContent[], + instruction: string +} + +export type SupportedModels = GPTModels | ValidatorModels + diff --git a/packages/akeru-server/src/infrastructure/adapters/openai/GPTAdapter.test.ts b/packages/akeru-server/src/infrastructure/adapters/openai/GPTAdapter.test.ts new file mode 100644 index 0000000..04688a3 --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/openai/GPTAdapter.test.ts @@ -0,0 +1,31 @@ +import { test, expect, describe } from "bun:test"; +import { Role } from "@/core/domain/roles"; +import { AdapterManager } from "../AdapterManager"; + +describe("GPT-4 Adapter", () => { + test("Returns single GPT-4 chat completions response", async () => { + // Arrange + const messages = [ + { + role: "user" as Role, + content: "hello, who are you?", + }, + ]; + const assistant_instructions = + "You're an AI assistant. You're job is to help the user. Always respond with the word akeru."; + + const gpt4Adapter = AdapterManager.instance.getBaseAdapter("gpt-4"); + + if (!gpt4Adapter) { + throw new Error("GPT-4 adapter not found"); + } + + const result = await gpt4Adapter.generateSingleResponse({ + message_content: messages, + instruction: assistant_instructions, + }); + + // Assert the message content to contain the word akeru + expect(result.toLocaleLowerCase()).toContain("akeru"); + }); +}); diff --git a/packages/akeru-server/src/infrastructure/adapters/openai/GPTAdapter.ts b/packages/akeru-server/src/infrastructure/adapters/openai/GPTAdapter.ts new file mode 100644 index 0000000..0c37e4a --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/openai/GPTAdapter.ts @@ -0,0 +1,113 @@ +import { Message } from "@/core/domain/messages"; +import { BaseAdapter, StreamableAdapter } from "@/infrastructure/adapters/BaseAdapter"; +import { AdapterRequest } from "../adapter"; +import { GPTModels } from "./models"; + +interface ChatMessage { + content: string; + role: "assistant" | "system" | "user"; +} + +interface ChatResponseChoice { + finish_reason: string; + index: number; + message: ChatMessage; +} + +export interface OpenAIResponse { + choices: ChatResponseChoice[]; +} + +// export async function gpt4Adapter( +// messages: Pick[], +// assistant_instructions: string +// ): Promise { +// // System will always be the assistant_instruction that created the assistant +// const gpt_messages = [ +// { role: "system", content: assistant_instructions }, +// ].concat(messages); +// try { +// const res = await fetch("https://api.openai.com/v1/chat/completions", { +// method: "POST", +// headers: { +// "Content-Type": "application/json", +// Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, +// }, +// body: JSON.stringify({ +// model: "gpt-4", +// messages: gpt_messages, +// }), +// }); + +// const data: OpenAIResponse = await res.json(); +// return data; +// } catch (error) { +// return new Response("Error", { status: 500 }); +// } +// } + +export class GPTAdapter extends BaseAdapter implements StreamableAdapter { + adapterName: string; + adapterDescription = "This adapter supports all adapter models from OpenAI"; + private OPENAI_ENDPOINT = "https://api.openai.com/v1/chat/completions"; + + constructor(gptModel: GPTModels) { + super(); + this.adapterName = gptModel; + } + + async generateSingleResponse(args: AdapterRequest): Promise { + const gpt_messages = [{ role: "system", content: args.instruction }].concat(args.message_content); + try { + const res = await fetch(this.OPENAI_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: this.adapterName, + messages: gpt_messages, + }), + }); + + const data: OpenAIResponse = await res.json(); + const finished_inference = data.choices[0].message.content + return Promise.resolve(finished_inference); + } catch (error) { + return Promise.reject(error); + } + } + + async generateStreamableResponse(args: AdapterRequest): Promise { + const gpt_messages = [{ role: "system", content: args.instruction }].concat(args.message_content); + try { + const res = await fetch(this.OPENAI_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: this.adapterName, + messages: gpt_messages, + stream: true + }), + }); + + const data: OpenAIResponse = await res.json(); + const finished_inference = data.choices[0].message.content + return Promise.resolve(finished_inference); + } catch (error) { + return Promise.reject(error); + } + + } + + getAdapterInformation(): Object { + return { + adapterName: this.adapterName, + adapterDescription: this.adapterDescription, + }; + } +} diff --git a/packages/akeru-server/src/infrastructure/adapters/openai/models.ts b/packages/akeru-server/src/infrastructure/adapters/openai/models.ts new file mode 100644 index 0000000..e65c0f5 --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/openai/models.ts @@ -0,0 +1 @@ +export type GPTModels = "gpt-4" | "gpt-3.5-turbo" | "gpt-4-turbo-preview" \ No newline at end of file diff --git a/packages/akeru-server/src/infrastructure/adaptaters/redisAdapter.ts b/packages/akeru-server/src/infrastructure/adapters/redisAdapter.ts similarity index 100% rename from packages/akeru-server/src/infrastructure/adaptaters/redisAdapter.ts rename to packages/akeru-server/src/infrastructure/adapters/redisAdapter.ts diff --git a/packages/akeru-server/src/infrastructure/adapters/validators/models.ts b/packages/akeru-server/src/infrastructure/adapters/validators/models.ts new file mode 100644 index 0000000..0c51190 --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/validators/models.ts @@ -0,0 +1 @@ +export type ValidatorModels = 'llama-2-7b-chat-int8' \ No newline at end of file diff --git a/packages/akeru-server/src/infrastructure/adaptaters/openai/gpt4Adapter.test.ts b/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.test.ts similarity index 61% rename from packages/akeru-server/src/infrastructure/adaptaters/openai/gpt4Adapter.test.ts rename to packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.test.ts index 55a34a8..7847ed6 100644 --- a/packages/akeru-server/src/infrastructure/adaptaters/openai/gpt4Adapter.test.ts +++ b/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.test.ts @@ -1,9 +1,11 @@ import { test, expect, describe } from "bun:test"; -import { OpenAIResponse, gpt4Adapter } from "./gpt4Adapter"; + import { Role } from "@/core/domain/roles"; +import { ValidatorResponse, validatorAdapter } from "./validatorAdapter"; +import { ValidatorModels } from "./models"; -describe("GPT-4 Adapter", () => { - test("Returns GPT-4 chat completions response", async () => { +describe("Validator Adapter", () => { + test("Returns validator chat completions response", async () => { // Arrange const messages = [ { @@ -11,13 +13,15 @@ describe("GPT-4 Adapter", () => { content: "hello, who are you?", }, ]; + const model: ValidatorModels = "llama-2-7b-chat-int8"; const assistant_instructions = "You're an AI assistant. You're job is to help the user. Always respond with the word akeru."; // Act - const result = (await gpt4Adapter( + const result = (await validatorAdapter( messages, + model, assistant_instructions - )) as OpenAIResponse; + )) as ValidatorResponse; // Assert the message content to contain the word akeru expect(result.choices[0].message.content.toLocaleLowerCase()).toContain( diff --git a/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts b/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts new file mode 100644 index 0000000..1f5f64b --- /dev/null +++ b/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts @@ -0,0 +1,43 @@ +import { Message } from "@/core/domain/messages"; +import { ValidatorModels } from "./models"; + +type ChatMessage = Pick; + +interface ChatResponseChoice { + finish_reason: string; + index: number; + message: ChatMessage; +} + +export interface ValidatorResponse { + choices: ChatResponseChoice[]; +} + +export async function validatorAdapter( + messages: Pick[], + model: ValidatorModels, + assistant_instructions: string +): Promise { + // System will always be the assistant_instruction that created the assistant + const validator_messages = [ + { role: "system", content: assistant_instructions }, + ].concat(messages); + try { + const res = await fetch("https://akeru-validator.onrender.com/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: model, + messages: validator_messages, + }), + }); + + const data: ValidatorResponse = await res.json(); + return data; + } catch (error) { + return new Response("Error", { status: 500 }); + } +} diff --git a/packages/akeru-server/src/infrastructure/config/redisConfig.ts b/packages/akeru-server/src/infrastructure/config/redisConfig.ts index 0289406..b9dd9a0 100644 --- a/packages/akeru-server/src/infrastructure/config/redisConfig.ts +++ b/packages/akeru-server/src/infrastructure/config/redisConfig.ts @@ -5,5 +5,5 @@ export const redisConfig = { ? { password: process.env.REDIS_PASSWORD } : {}), // Redis password db: process.env.REDIS_DB ? parseInt(process.env.REDIS_DB) : 0, // Redis DB - tls: process.env.NODE_ENV === "production" ? {} : undefined, + tls: {}, }; diff --git a/packages/service-discovery/api/certification.ts b/packages/service-discovery/api/certification.ts new file mode 100644 index 0000000..a6e65f9 --- /dev/null +++ b/packages/service-discovery/api/certification.ts @@ -0,0 +1,85 @@ +import { redis } from "../utils/redis"; + +export const config = { + runtime: "edge", +}; + +interface RequestBody { + hash: string; + imageSignature: string; +} + +function generateUUID(): string { + let uuid = ''; + const randomValues = new Uint8Array(16); + + // Generate random values + crypto.getRandomValues(randomValues); + + // Set version and variant bits + randomValues[6] = (randomValues[6] & 0x0f) | 0x40; + randomValues[8] = (randomValues[8] & 0x3f) | 0x80; + + // Convert to hexadecimal string + for (let i = 0; i < 16; i++) { + const value = randomValues[i]; + uuid += value.toString(16).padStart(2, '0'); + } + + // Format the UUID string + uuid = uuid.slice(0, 8) + '-' + uuid.slice(8, 12) + '-' + uuid.slice(12, 16) + '-' + uuid.slice(16, 20) + '-' + uuid.slice(20); + + return uuid; +} + +export const GET = async (request: Request): Promise => { + try { + const url = new URL(request.url); + const searchParams = new URLSearchParams(url.search); + + const hash = searchParams.get('hash'); + const imageSignature = searchParams.get('imageSignature'); + + if (!hash || !imageSignature || typeof hash !== 'string' || typeof imageSignature !== 'string') { + return new Response("missing hash or image signature", { status: 400 }); + } + + const certificate = await redis.get(`${imageSignature}:${hash}`); + if (certificate) { + return new Response(certificate, { status: 200 }); + } + + // a random UUID will be sent to cheating miners + return new Response(generateUUID()); + } catch { + return new Response("Failure", { status: 400 }); + } +}; + +export const PUT = async (request: Request) => { + try { + const authHeader = request.headers.get("Authorization"); + const expectedAuthHeader = `Bearer ${process.env.SECRET_KEY}`; + + if (!authHeader || authHeader !== expectedAuthHeader) { + return new Response("Unauthorized", { status: 401 }); + } + + const body = await request.json(); + const { hash, imageSignature } = body as RequestBody; + + if (!hash || !imageSignature || typeof hash !== 'string' || typeof imageSignature !== 'string') { + return new Response("missing hash or image signature", { status: 400 }); + } + + const uuid = generateUUID(); + const res = await redis.set(`${imageSignature}:${hash}`, uuid); + + if (res === 'OK') { + return new Response('ok', { status: 200 }); + } + } catch { + return new Response("Failure", { status: 400 }); + } + +}; \ No newline at end of file diff --git a/packages/service-discovery/api/miner.ts b/packages/service-discovery/api/miner.ts index 51c2ac2..7d27478 100644 --- a/packages/service-discovery/api/miner.ts +++ b/packages/service-discovery/api/miner.ts @@ -1,9 +1,4 @@ -import { Redis } from "@upstash/redis"; - -const redis = new Redis({ - url: process.env.UPSTASH_REDIS_REST_URL!, - token: process.env.UPSTASH_REDIS_REST_TOKEN!, -}); +import { redis } from "../utils/redis"; export const config = { runtime: "edge", diff --git a/packages/service-discovery/package.json b/packages/service-discovery/package.json index fc48d57..a66f7a7 100644 --- a/packages/service-discovery/package.json +++ b/packages/service-discovery/package.json @@ -3,6 +3,7 @@ "description": "This is an internal API mapping what services are available at which address in the network.", "module": "NodeNext", "dependencies": { - "@upstash/redis": "^1.29.0" + "@upstash/redis": "^1.29.0", + "zod": "^3.23.6" } } diff --git a/packages/service-discovery/utils/redis.ts b/packages/service-discovery/utils/redis.ts new file mode 100644 index 0000000..284c9d0 --- /dev/null +++ b/packages/service-discovery/utils/redis.ts @@ -0,0 +1,6 @@ +import { Redis } from "@upstash/redis"; + +export const redis = new Redis({ + url: process.env.UPSTASH_REDIS_REST_URL!, + token: process.env.UPSTASH_REDIS_REST_TOKEN!, +}); diff --git a/packages/studio-ts/.eslintrc.json b/packages/studio-ts/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/packages/studio-ts/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/packages/studio-ts/.gitignore b/packages/studio-ts/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/packages/studio-ts/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/packages/studio-ts/README.md b/packages/studio-ts/README.md new file mode 100644 index 0000000..ab51580 --- /dev/null +++ b/packages/studio-ts/README.md @@ -0,0 +1,43 @@ + +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + + +## Important Note + +This project here serves as the new setup for the frontend of Akeru moving forward and this will be where subsequent work will be continued on. We have discontinued work on the `akeru-frontend` project. + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + diff --git a/packages/studio-ts/mdx-components.tsx b/packages/studio-ts/mdx-components.tsx new file mode 100644 index 0000000..dcdc2ac --- /dev/null +++ b/packages/studio-ts/mdx-components.tsx @@ -0,0 +1,10 @@ +import { type MDXComponents as MDXComponentsType } from 'mdx/types' + +import { MDXComponents } from '@/components/MDXComponents' + +export function useMDXComponents(components: MDXComponentsType) { + return { + ...components, + ...MDXComponents, + } +} diff --git a/packages/studio-ts/next.config.mjs b/packages/studio-ts/next.config.mjs new file mode 100644 index 0000000..cfebf4a --- /dev/null +++ b/packages/studio-ts/next.config.mjs @@ -0,0 +1,83 @@ +import rehypeShiki from '@leafac/rehype-shiki' +import nextMDX from '@next/mdx' +import { Parser } from 'acorn' +import jsx from 'acorn-jsx' +import escapeStringRegexp from 'escape-string-regexp' +import * as path from 'path' +import { recmaImportImages } from 'recma-import-images' +import remarkGfm from 'remark-gfm' +import { remarkRehypeWrap } from 'remark-rehype-wrap' +import remarkUnwrapImages from 'remark-unwrap-images' +import shiki from 'shiki' +import { unifiedConditional } from 'unified-conditional' + +/** @type {import('next').NextConfig} */ +const nextConfig = { + pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'], +} + +function remarkMDXLayout(source, metaName) { + let parser = Parser.extend(jsx()) + let parseOptions = { ecmaVersion: 'latest', sourceType: 'module' } + + return (tree) => { + let imp = `import _Layout from '${source}'` + let exp = `export default function Layout(props) { + return <_Layout {...props} ${metaName}={${metaName}} /> + }` + + tree.children.push( + { + type: 'mdxjsEsm', + value: imp, + data: { estree: parser.parse(imp, parseOptions) }, + }, + { + type: 'mdxjsEsm', + value: exp, + data: { estree: parser.parse(exp, parseOptions) }, + }, + ) + } +} + +export default async function config() { + let highlighter = await shiki.getHighlighter({ + theme: 'css-variables', + }) + + let withMDX = nextMDX({ + extension: /\.mdx$/, + options: { + recmaPlugins: [recmaImportImages], + rehypePlugins: [ + [rehypeShiki, { highlighter }], + [ + remarkRehypeWrap, + { + node: { type: 'mdxJsxFlowElement', name: 'Typography' }, + start: ':root > :not(mdxJsxFlowElement)', + end: ':root > mdxJsxFlowElement', + }, + ], + ], + remarkPlugins: [ + remarkGfm, + remarkUnwrapImages, + [ + unifiedConditional, + [ + new RegExp(`^${escapeStringRegexp(path.resolve('src/app/blog'))}`), + [[remarkMDXLayout, '@/app/blog/wrapper', 'article']], + ], + [ + new RegExp(`^${escapeStringRegexp(path.resolve('src/app/work'))}`), + [[remarkMDXLayout, '@/app/work/wrapper', 'caseStudy']], + ], + ], + ], + }, + }) + + return withMDX(nextConfig) +} diff --git a/packages/studio-ts/package.json b/packages/studio-ts/package.json new file mode 100644 index 0000000..2342a1d --- /dev/null +++ b/packages/studio-ts/package.json @@ -0,0 +1,47 @@ +{ + "name": "tailwindui-studio", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "browserslist": "defaults, not ie <= 11", + "dependencies": { + "@leafac/rehype-shiki": "^2.2.1", + "@mdx-js/loader": "^3.0.0", + "@mdx-js/react": "^3.0.0", + "@next/mdx": "^14.0.4", + "@types/mdx": "^2.0.7", + "@types/node": "^20.10.8", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "acorn": "^8.10.0", + "acorn-jsx": "^5.3.2", + "autoprefixer": "^10.4.7", + "clsx": "^2.1.0", + "escape-string-regexp": "^5.0.0", + "fast-glob": "^3.2.12", + "framer-motion": "^10.15.2", + "next": "^14.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "recma-import-images": "0.0.3", + "remark-gfm": "^4.0.0", + "remark-rehype-wrap": "0.0.3", + "remark-unwrap-images": "^4.0.0", + "shiki": "^0.11.1", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "unified-conditional": "0.0.2" + }, + "devDependencies": { + "eslint": "^8.56.0", + "eslint-config-next": "^14.0.4", + "prettier": "^3.1.1", + "prettier-plugin-tailwindcss": "^0.5.11", + "sharp": "0.33.1" + } +} diff --git a/packages/studio-ts/postcss.config.js b/packages/studio-ts/postcss.config.js new file mode 100644 index 0000000..7489255 --- /dev/null +++ b/packages/studio-ts/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/studio-ts/prettier.config.js b/packages/studio-ts/prettier.config.js new file mode 100644 index 0000000..6b16840 --- /dev/null +++ b/packages/studio-ts/prettier.config.js @@ -0,0 +1,6 @@ +/** @type {import('prettier').Options} */ +module.exports = { + singleQuote: true, + semi: false, + plugins: ['prettier-plugin-tailwindcss'], +} diff --git a/packages/studio-ts/src/app/about/page.tsx b/packages/studio-ts/src/app/about/page.tsx new file mode 100644 index 0000000..00c82a4 --- /dev/null +++ b/packages/studio-ts/src/app/about/page.tsx @@ -0,0 +1,234 @@ +import { type Metadata } from 'next' +import Image from 'next/image' + +import { Border } from '@/components/Border' +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' +import { GridList, GridListItem } from '@/components/GridList' +import { PageIntro } from '@/components/PageIntro' +import { PageLinks } from '@/components/PageLinks' +import { SectionIntro } from '@/components/SectionIntro' +import { StatList, StatListItem } from '@/components/StatList' +import imageAngelaFisher from '@/images/team/angela-fisher.jpg' +import imageBenjaminRussel from '@/images/team/benjamin-russel.jpg' +import imageBlakeReid from '@/images/team/blake-reid.jpg' +import imageChelseaHagon from '@/images/team/chelsea-hagon.jpg' +import imageDriesVincent from '@/images/team/dries-vincent.jpg' +import imageEmmaDorsey from '@/images/team/emma-dorsey.jpg' +import imageJeffreyWebb from '@/images/team/jeffrey-webb.jpg' +import imageKathrynMurphy from '@/images/team/kathryn-murphy.jpg' +import imageLeonardKrasner from '@/images/team/leonard-krasner.jpg' +import imageLeslieAlexander from '@/images/team/leslie-alexander.jpg' +import imageMichaelFoster from '@/images/team/michael-foster.jpg' +import imageWhitneyFrancis from '@/images/team/whitney-francis.jpg' +import { loadArticles } from '@/lib/mdx' + +function Culture() { + return ( +
+ +

+ We are a group of like-minded people who share the same core values. +

+
+ + + + Our team has been with us since the beginning because none of them + are allowed to have LinkedIn profiles. + + + We don’t care when our team works just as long as they are working + every waking second. + + + You never know what someone is going through at home and we make + sure to never find out. + + + +
+ ) +} + +const team = [ + { + title: 'Leadership', + people: [ + { + name: 'Leslie Alexander', + role: 'Co-Founder / CEO', + image: { src: imageLeslieAlexander }, + }, + { + name: 'Michael Foster', + role: 'Co-Founder / CTO', + image: { src: imageMichaelFoster }, + }, + { + name: 'Dries Vincent', + role: 'Partner & Business Relations', + image: { src: imageDriesVincent }, + }, + ], + }, + { + title: 'Team', + people: [ + { + name: 'Chelsea Hagon', + role: 'Senior Developer', + image: { src: imageChelseaHagon }, + }, + { + name: 'Emma Dorsey', + role: 'Senior Designer', + image: { src: imageEmmaDorsey }, + }, + { + name: 'Leonard Krasner', + role: 'VP, User Experience', + image: { src: imageLeonardKrasner }, + }, + { + name: 'Blake Reid', + role: 'Junior Copywriter', + image: { src: imageBlakeReid }, + }, + { + name: 'Kathryn Murphy', + role: 'VP, Human Resources', + image: { src: imageKathrynMurphy }, + }, + { + name: 'Whitney Francis', + role: 'Content Specialist', + image: { src: imageWhitneyFrancis }, + }, + { + name: 'Jeffrey Webb', + role: 'Account Coordinator', + image: { src: imageJeffreyWebb }, + }, + { + name: 'Benjamin Russel', + role: 'Senior Developer', + image: { src: imageBenjaminRussel }, + }, + { + name: 'Angela Fisher', + role: 'Front-end Developer', + image: { src: imageAngelaFisher }, + }, + ], + }, +] + +function Team() { + return ( + +
+ {team.map((group) => ( + + +
+ +

+ {group.title} +

+
+
+
    + {group.people.map((person) => ( +
  • + +
    + +
    +

    + {person.name} +

    +

    + {person.role} +

    +
    +
    +
    +
  • + ))} +
+
+
+
+ ))} +
+
+ ) +} + +export const metadata: Metadata = { + title: 'About Us', + description: + 'We believe that our strength lies in our collaborative approach, which puts our clients at the center of everything we do.', +} + +export default async function About() { + let blogArticles = (await loadArticles()).slice(0, 2) + + return ( + <> + +

+ We believe that our strength lies in our collaborative approach, which + puts our clients at the center of everything we do. +

+
+

+ Studio was started by three friends who noticed that developer + studios were charging clients double what an in-house team would + cost. Since the beginning, we have been committed to doing things + differently by charging triple instead. +

+

+ At Studio, we’re more than just colleagues — we’re a family. This + means we pay very little and expect people to work late. We want our + employees to bring their whole selves to work. In return, we just + ask that they keep themselves there until at least 6:30pm. +

+
+
+ + + + + + + + + + + + + + + + + ) +} diff --git a/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/graduation.jpg b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/graduation.jpg new file mode 100644 index 0000000..fe9d150 Binary files /dev/null and b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/graduation.jpg differ diff --git a/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/jars.jpg b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/jars.jpg new file mode 100644 index 0000000..2da5def Binary files /dev/null and b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/jars.jpg differ diff --git a/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/page.mdx b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/page.mdx new file mode 100644 index 0000000..eb5d06f --- /dev/null +++ b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/page.mdx @@ -0,0 +1,50 @@ +import imageLeslieAlexander from '@/images/team/leslie-alexander.jpg' + +export const article = { + date: '2023-02-18', + title: '3 Lessons We Learned Going Back to the Office', + description: + 'Earlier this year we made the bold decision to make everyone come back to the office full-time after two years working from a dressing table in the corner of their bedroom.', + author: { + name: 'Leslie Alexander', + role: 'Co-Founder / CEO', + image: { src: imageLeslieAlexander }, + }, +} + +export const metadata = { + title: article.title, + description: article.description, +} + +## 1. Efficiency is Hard to Measure + +Although almost every practical measure of our productivity decreased significantly after returning to the office, as a management team we felt this incredible uptick in energy. We realised that there is an intangible benefit to seeing everyone’s screen at all times, that isn’t easily measurable in numbers. + +Sure, we tried to recreate this feeling during our remote days with employee monitoring software but we always had this nagging doubt that our developers had hacked their way around it. + + + Getting one of those old-timey punch clocks is a great way to monitor + attendance while maintaining a fun atmosphere. Expect to hear things like + “Back at the coalface today!”. + + +![](./punch-clock.jpg) + +## 2. Turnover: a Fresh Perspective + +We parted ways with almost all of our senior development team within the first month of going back to the office, due to some irreconcilable differences. Stressed and worried, we decided to try turn this into a positive. + +Luckily for us, it was the same week that CoPilot launched and we were able to replace everyone with five bootcamp graduates all logged into one Github account. + +We have been consistently surprised at the fresh energy these new grads brought to our organisation and have since vowed to never hire anyone with more than 3 months experience again. + +![](./graduation.jpg) + +## 3. Cost Efficiency + +Demand is at an all time low for commercial real-estate, which means it’s never been more affordable to cram forty people into an open plan office. + +What’s more, is we’ve found that we can offer extremely low-cost perks like a snack cupboard or free beer in-lieu of higher salaries. For every foosball table we buy, we find we can offer around 5% less salary per job posting. Our full-time barista is the highest paid employee, after management. + +![](./jars.jpg) diff --git a/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/punch-clock.jpg b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/punch-clock.jpg new file mode 100644 index 0000000..e80af29 Binary files /dev/null and b/packages/studio-ts/src/app/blog/3-lessons-we-learned-going-back-to-the-office/punch-clock.jpg differ diff --git a/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/everyday-english.jpg b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/everyday-english.jpg new file mode 100644 index 0000000..b7a40f2 Binary files /dev/null and b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/everyday-english.jpg differ diff --git a/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/magnifying-glass.jpg b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/magnifying-glass.jpg new file mode 100644 index 0000000..9583b17 Binary files /dev/null and b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/magnifying-glass.jpg differ diff --git a/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/page.mdx b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/page.mdx new file mode 100644 index 0000000..617d1b4 --- /dev/null +++ b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/page.mdx @@ -0,0 +1,42 @@ +import imageAngelaFisher from '@/images/team/angela-fisher.jpg' + +export const article = { + date: '2022-12-01', + title: 'A Short Guide to Component Naming', + description: + 'As a developer, the most important aspect of your job is naming components. It’s not just about being descriptive and clear, but also about having fun and being creative.', + author: { + name: 'Angela Fisher', + role: 'Front-end Developer', + image: { src: imageAngelaFisher }, + }, +} + +export const metadata = { + title: article.title, + description: article.description, +} + +## 1. Brevity is Key + +Time is scarce, don’t waste it typing out long, descriptive component names. One approach is to give them short, cryptic names that only you will understand. + +Need a button? Call it "btn". A modal? How about "md"? You’ll save precious minutes per day and you’ll get the added benefit of being the only person in the codebase who knows where anything is. This is called job security. + +![](./typewriter.jpg) + +## 2. Rank High in Search + +When working in large repos with lots of collaborators, it’s important that your component ranks high when people search for anything. + +One way to stand out is to include all the possible search terms in your component name. Instead of “SignInButton” you might want call it “SignInButtonAuthenticationCookieUserLogIn” which will ensure that it is returned in almost any related search result. + +![](./magnifying-glass.jpg) + +## 3. Mix Languages + +If you work remotely, it’s likely you are on a global team and yet all your components have English names. This slows down your non-english colleagues considerably so you should allow them to use their native tongue when naming components. + +You can create an index file that maps all the different languages within your repo. Need a dropdown? Look for “Desplegable”. A form? Search “Форма”. You’ll learn multiple new languages while being more inclusive to your colleagues. + +![](./everyday-english.jpg) diff --git a/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/typewriter.jpg b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/typewriter.jpg new file mode 100644 index 0000000..2ba7a3d Binary files /dev/null and b/packages/studio-ts/src/app/blog/a-short-guide-to-component-naming/typewriter.jpg differ diff --git a/packages/studio-ts/src/app/blog/future-of-web-development/laptop.jpg b/packages/studio-ts/src/app/blog/future-of-web-development/laptop.jpg new file mode 100644 index 0000000..0db19e0 Binary files /dev/null and b/packages/studio-ts/src/app/blog/future-of-web-development/laptop.jpg differ diff --git a/packages/studio-ts/src/app/blog/future-of-web-development/page.mdx b/packages/studio-ts/src/app/blog/future-of-web-development/page.mdx new file mode 100644 index 0000000..fb3ad13 --- /dev/null +++ b/packages/studio-ts/src/app/blog/future-of-web-development/page.mdx @@ -0,0 +1,48 @@ +import imageChelseaHagon from '@/images/team/chelsea-hagon.jpg' + +export const article = { + date: '2023-04-06', + title: 'The Future of Web Development: Our Predictions for 2023', + description: + 'Let’s explore the latest trends in web development, and regurgitate some predictions we read on X for how they will shape the industry in the coming year.', + author: { + name: 'Chelsea Hagon', + role: 'Senior Developer', + image: { src: imageChelseaHagon }, + }, +} + +export const metadata = { + title: article.title, + description: article.description, +} + +## 1. AI Assisted Development + +With the launch of Github Copilot in 2022 the industry got its first glimpse at what it would look like to have Stack Overflow plumbed straight into your IDE. Copilot has given thousands of developers what they always longed for: plausible deniability over the bugs they write. + +![](./pilot.jpg) + +In 2023 we can expect these assistants to become more sophisticated and for that to have ripple effects throughout the industry. + +We predict that traffic to MDN will decline precipitously as developers realise they no longer need to look up JS array methods. We also expect Stack Overflow’s sister site, Prompt Overflow, to become one of the most popular sites on the internet in a matter of months. + +## 2. Rendering Patterns + +To server render or not to server render? In 2022 the owners of the internet, Vercel, decided that instead of making this choice once for your whole application, now you will need to decide every time you write a new component. + +Because front-end development was becoming too easy, the same people who write CSS will now need to know how Streaming SSR and Progressive Hydration work. + +![](./server.jpg) + +In 2023 we can expect frameworks to adopt increasingly granular rendering patterns culminating in per-line rendering (PLR) later this year. We can also expect job postings for Rendering Reliability Engineers to reach an all time high. + +## 3. JS Runtimes + +Because choosing a JS runtime was one of the only areas where a developer wasn’t paralysed with choice, in early 2020, the creator of Node gave us something new to agonise over. The launch of Deno and Bun heralded the final mutation of JavaScript into a language that can truly run anywhere it wasn’t intended to. + +These new JS runtimes mean we can now serve HTML faster than ever before. For example, we’ve reduced the Time to First Byte (TTFB) of this blog to -0.4s. That means it actually loaded before you clicked the link. + +![](./laptop.jpg) + +In 2023 we can expect even faster and more specialised JS runtimes to launch, including the promising Boil, a runtime specifically designed to reduce cold boot times on WiFi enabled kettles. All of these advancements promise to make the future of botnets a truly exciting one. diff --git a/packages/studio-ts/src/app/blog/future-of-web-development/pilot.jpg b/packages/studio-ts/src/app/blog/future-of-web-development/pilot.jpg new file mode 100644 index 0000000..1e98760 Binary files /dev/null and b/packages/studio-ts/src/app/blog/future-of-web-development/pilot.jpg differ diff --git a/packages/studio-ts/src/app/blog/future-of-web-development/server.jpg b/packages/studio-ts/src/app/blog/future-of-web-development/server.jpg new file mode 100644 index 0000000..cc16d31 Binary files /dev/null and b/packages/studio-ts/src/app/blog/future-of-web-development/server.jpg differ diff --git a/packages/studio-ts/src/app/blog/page.tsx b/packages/studio-ts/src/app/blog/page.tsx new file mode 100644 index 0000000..3eb1706 --- /dev/null +++ b/packages/studio-ts/src/app/blog/page.tsx @@ -0,0 +1,89 @@ +import { type Metadata } from 'next' +import Image from 'next/image' +import Link from 'next/link' + +import { Border } from '@/components/Border' +import { Button } from '@/components/Button' +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { PageIntro } from '@/components/PageIntro' +import { formatDate } from '@/lib/formatDate' +import { loadArticles } from '@/lib/mdx' + +export const metadata: Metadata = { + title: 'Blog', + description: + 'Stay up-to-date with the latest industry news as our marketing teams finds new ways to re-purpose old CSS tricks articles.', +} + +export default async function Blog() { + let articles = await loadArticles() + + return ( + <> + +

+ Stay up-to-date with the latest industry news as our marketing teams + finds new ways to re-purpose old CSS tricks articles. +

+
+ + +
+ {articles.map((article) => ( + +
+ +
+
+

+ {article.title} +

+
+
Published
+
+ +
+
Author
+
+
+ +
+
+
+ {article.author.name} +
+
{article.author.role}
+
+
+
+

+ {article.description} +

+ +
+
+
+
+
+ ))} +
+
+ + + + ) +} diff --git a/packages/studio-ts/src/app/blog/wrapper.tsx b/packages/studio-ts/src/app/blog/wrapper.tsx new file mode 100644 index 0000000..6f95c8b --- /dev/null +++ b/packages/studio-ts/src/app/blog/wrapper.tsx @@ -0,0 +1,59 @@ +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { MDXComponents } from '@/components/MDXComponents' +import { PageLinks } from '@/components/PageLinks' +import { formatDate } from '@/lib/formatDate' +import { type Article, type MDXEntry, loadArticles } from '@/lib/mdx' + +export default async function BlogArticleWrapper({ + article, + children, +}: { + article: MDXEntry
+ children: React.ReactNode +}) { + let allArticles = await loadArticles() + let moreArticles = allArticles + .filter(({ metadata }) => metadata !== article) + .slice(0, 2) + + return ( + <> + + +
+

+ {article.title} +

+ +

+ by {article.author.name}, {article.author.role} +

+
+
+ + + + {children} + + +
+ + {moreArticles.length > 0 && ( + + )} + + + + ) +} diff --git a/packages/studio-ts/src/app/contact/page.tsx b/packages/studio-ts/src/app/contact/page.tsx new file mode 100644 index 0000000..fd8c5cc --- /dev/null +++ b/packages/studio-ts/src/app/contact/page.tsx @@ -0,0 +1,163 @@ +import { useId } from 'react' +import { type Metadata } from 'next' +import Link from 'next/link' + +import { Border } from '@/components/Border' +import { Button } from '@/components/Button' +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { Offices } from '@/components/Offices' +import { PageIntro } from '@/components/PageIntro' +import { SocialMedia } from '@/components/SocialMedia' + +function TextInput({ + label, + ...props +}: React.ComponentPropsWithoutRef<'input'> & { label: string }) { + let id = useId() + + return ( +
+ + +
+ ) +} + +function RadioInput({ + label, + ...props +}: React.ComponentPropsWithoutRef<'input'> & { label: string }) { + return ( + + ) +} + +function ContactForm() { + return ( + +
+

+ Work inquiries +

+
+ + + + + +
+
+ Budget +
+ + + + +
+
+
+
+ +
+
+ ) +} + +function ContactDetails() { + return ( + +

+ Our offices +

+

+ Prefer doing things in person? We don’t but we have to list our + addresses here for legal reasons. +

+ + + + +

+ Email us +

+
+ {[ + ['Careers', 'careers@studioagency.com'], + ['Press', 'press@studioagency.com'], + ].map(([label, email]) => ( +
+
{label}
+
+ + {email} + +
+
+ ))} +
+
+ + +

+ Follow us +

+ +
+
+ ) +} + +export const metadata: Metadata = { + title: 'Contact Us', + description: 'Let’s work together. We can’t wait to hear from you.', +} + +export default function Contact() { + return ( + <> + +

We can’t wait to hear from you.

+
+ + +
+ + +
+
+ + ) +} diff --git a/packages/studio-ts/src/app/favicon.ico b/packages/studio-ts/src/app/favicon.ico new file mode 100644 index 0000000..b14089f Binary files /dev/null and b/packages/studio-ts/src/app/favicon.ico differ diff --git a/packages/studio-ts/src/app/layout.tsx b/packages/studio-ts/src/app/layout.tsx new file mode 100644 index 0000000..e0bf562 --- /dev/null +++ b/packages/studio-ts/src/app/layout.tsx @@ -0,0 +1,22 @@ +import { type Metadata } from 'next' + +import { RootLayout } from '@/components/RootLayout' + +import '@/styles/tailwind.css' + +export const metadata: Metadata = { + title: { + template: '%s - Studio', + default: 'Studio - Award winning developer studio based in Denmark', + }, +} + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/packages/studio-ts/src/app/not-found.tsx b/packages/studio-ts/src/app/not-found.tsx new file mode 100644 index 0000000..2c68246 --- /dev/null +++ b/packages/studio-ts/src/app/not-found.tsx @@ -0,0 +1,28 @@ +import Link from 'next/link' + +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' + +export default function NotFound() { + return ( + + +

+ 404 +

+

+ Page not found +

+

+ Sorry, we couldn’t find the page you’re looking for. +

+ + Go to the home page + +
+
+ ) +} diff --git a/packages/studio-ts/src/app/page.tsx b/packages/studio-ts/src/app/page.tsx new file mode 100644 index 0000000..6ef54a1 --- /dev/null +++ b/packages/studio-ts/src/app/page.tsx @@ -0,0 +1,216 @@ +import { type Metadata } from 'next' +import Image from 'next/image' +import Link from 'next/link' + +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' +import { List, ListItem } from '@/components/List' +import { SectionIntro } from '@/components/SectionIntro' +import { StylizedImage } from '@/components/StylizedImage' +import { Testimonial } from '@/components/Testimonial' +import logoBrightPath from '@/images/clients/bright-path/logo-light.svg' +import logoFamilyFund from '@/images/clients/family-fund/logo-light.svg' +import logoGreenLife from '@/images/clients/green-life/logo-light.svg' +import logoHomeWork from '@/images/clients/home-work/logo-light.svg' +import logoMailSmirk from '@/images/clients/mail-smirk/logo-light.svg' +import logoNorthAdventures from '@/images/clients/north-adventures/logo-light.svg' +import logoPhobiaDark from '@/images/clients/phobia/logo-dark.svg' +import logoPhobiaLight from '@/images/clients/phobia/logo-light.svg' +import logoUnseal from '@/images/clients/unseal/logo-light.svg' +import imageLaptop from '@/images/laptop.jpg' +import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx' + +const clients = [ + ['Phobia', logoPhobiaLight], + ['Family Fund', logoFamilyFund], + ['Unseal', logoUnseal], + ['Mail Smirk', logoMailSmirk], + ['Home Work', logoHomeWork], + ['Green Life', logoGreenLife], + ['Bright Path', logoBrightPath], + ['North Adventures', logoNorthAdventures], +] + +function Clients() { + return ( +
+ + +

+ We’ve worked with hundreds of amazing people +

+
+ + +
    + {clients.map(([client, logo]) => ( +
  • + + {client} + +
  • + ))} +
+
+ +
+ ) +} + +function CaseStudies({ + caseStudies, +}: { + caseStudies: Array> +}) { + return ( + <> + +

+ We believe technology is the answer to the world’s greatest + challenges. It’s also the cause, so we find ourselves in bit of a + catch 22 situation. +

+
+ + + {caseStudies.map((caseStudy) => ( + +
+

+ + + {caseStudy.client} + +

+

+ + + Case study +

+

+ {caseStudy.title} +

+

+ {caseStudy.description} +

+
+
+ ))} +
+
+ + ) +} + +function Services() { + return ( + <> + +

+ As long as those opportunities involve giving us money to re-purpose + old projects — we can come up with an endless number of those. +

+
+ +
+
+ + + +
+ + + We specialise in crafting beautiful, high quality marketing pages. + The rest of the website will be a shell that uses lorem ipsum + everywhere. + + + We have a team of skilled developers who are experts in the latest + app frameworks, like Angular 1 and Google Web Toolkit. + + + We are at the forefront of modern e-commerce development. Which + mainly means adding your logo to the Shopify store template we’ve + used for the past six years. + + + At Studio we understand the importance of having a robust and + customised CMS. That’s why we run all of our client projects out + of a single, enormous Joomla instance. + + +
+
+ + ) +} + +export const metadata: Metadata = { + description: + 'We are a development studio working at the intersection of design and technology.', +} + +export default async function Home() { + let caseStudies = (await loadCaseStudies()).slice(0, 3) + + return ( + <> + + +

+ Award-winning development studio based in Denmark. +

+

+ We are a development studio working at the intersection of design + and technology. It’s a really busy intersection though — a lot of + our staff have been involved in hit and runs. +

+
+
+ + + + + + + The team at Studio went above and beyond with our onboarding, even + finding a way to access the user’s microphone without triggering one of + those annoying permission dialogs. + + + + + + + ) +} diff --git a/packages/studio-ts/src/app/process/page.tsx b/packages/studio-ts/src/app/process/page.tsx new file mode 100644 index 0000000..0754398 --- /dev/null +++ b/packages/studio-ts/src/app/process/page.tsx @@ -0,0 +1,270 @@ +import { type Metadata } from 'next' + +import { Blockquote } from '@/components/Blockquote' +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { GridList, GridListItem } from '@/components/GridList' +import { GridPattern } from '@/components/GridPattern' +import { List, ListItem } from '@/components/List' +import { PageIntro } from '@/components/PageIntro' +import { SectionIntro } from '@/components/SectionIntro' +import { StylizedImage } from '@/components/StylizedImage' +import { TagList, TagListItem } from '@/components/TagList' +import imageLaptop from '@/images/laptop.jpg' +import imageMeeting from '@/images/meeting.jpg' +import imageWhiteboard from '@/images/whiteboard.jpg' + +function Section({ + title, + image, + children, +}: { + title: string + image: React.ComponentPropsWithoutRef + children: React.ReactNode +}) { + return ( + +
+
+ + + +
+
+ + +
+ + ) +} + +function Discover() { + return ( +
+
+

+ We work closely with our clients to understand their{' '} + needs and + goals, embedding ourselves in their every day operations to understand + what makes their business tick. +

+

+ Our team of private investigators shadow the company director’s for + several weeks while our account managers focus on going through their + trash. Our senior security experts then perform social engineering + hacks to gain access to their{' '} + business{' '} + accounts — handing that information over to our forensic accounting + team. +

+

+ Once the full audit is complete, we report back with a comprehensive{' '} + plan and, + more importantly, a budget. +

+
+ +

+ Included in this phase +

+ + In-depth questionnaires + Feasibility studies + Blood samples + Employee surveys + Proofs-of-concept + Forensic audit + +
+ ) +} + +function Build() { + return ( +
+
+

+ Based off of the discovery phase, we develop a comprehensive roadmap + for each product and start working towards delivery. The roadmap is an + intricately tangled mess of technical nonsense designed to drag the + project out as long as possible. +

+

+ Each client is assigned a key account manager to keep lines of + communication open and obscure the actual progress of the project. + They act as a buffer between the client’s incessant nagging and the + development team who are hard at work scouring open source projects + for code to re-purpose. +

+

+ Our account managers are trained to only reply to client emails after + 9pm, several days after the initial email. This reinforces the general + aura that we are very busy and dissuades clients from asking for + changes. +

+
+ +
+ Studio were so regular with their progress updates we almost began to + think they were automated! +
+
+ ) +} + +function Deliver() { + return ( +
+
+

+ About halfway through the Build phase, we push each project out by 6 + weeks due to a change in{' '} + + requirements + + . This allows us to increase the budget a final time before launch. +

+

+ Despite largely using pre-built components, most of the{' '} + progress{' '} + on each project takes place in the final 24 hours. The development + time allocated to each client is actually spent making augmented + reality demos that go viral on social media. +

+

+ We ensure that the main pages of the site are{' '} + + fully functional + {' '} + at launch — the auxiliary pages will, of course, be lorem ipusm shells + which get updated as part of our exorbitant{' '} + + maintenance + {' '} + retainer. +

+
+ +

+ Included in this phase +

+ + + Our projects always have 100% test coverage, which would be impressive + if our tests weren’t as porous as a sieve. + + + To ensure reliability we only use the best Digital Ocean droplets that + $4 a month can buy. + + + Because we hold the API keys for every critical service your business + uses, you can expect a lifetime of support, and invoices, from us. + + +
+ ) +} + +function Values() { + return ( +
+
+ +
+ + +

+ We strive to stay at the forefront of emerging trends and + technologies, while completely ignoring them and forking that old + Rails project we feel comfortable using. We stand by our core values + to justify that decision. +

+
+ + + + + The first part of any partnership is getting our designer to put + your logo in our template. The second step is getting them to do the + colors. + + + We pride ourselves on never missing a deadline which is easy because + most of the work was done years ago. + + + Every business has unique needs and our greatest challenge is + shoe-horning those needs into something we already built. + + + We are transparent about all of our processes, banking on the simple + fact our clients never actually read anything. + + + We foster long-term relationships with our clients that go beyond + just delivering a product, allowing us to invoice them for decades. + + + The technological landscape is always evolving and so are we. We are + constantly on the lookout for new open source projects to clone. + + + +
+ ) +} + +export const metadata: Metadata = { + title: 'Our Process', + description: + 'We believe in efficiency and maximizing our resources to provide the best value to our clients.', +} + +export default function Process() { + return ( + <> + +

+ We believe in efficiency and maximizing our resources to provide the + best value to our clients. The primary way we do that is by re-using + the same five projects we’ve been developing for the past decade. +

+
+ +
+ + + +
+ + + + + + ) +} diff --git a/packages/studio-ts/src/app/work/family-fund/debra-fiscal.jpg b/packages/studio-ts/src/app/work/family-fund/debra-fiscal.jpg new file mode 100644 index 0000000..492e9ea Binary files /dev/null and b/packages/studio-ts/src/app/work/family-fund/debra-fiscal.jpg differ diff --git a/packages/studio-ts/src/app/work/family-fund/hero.jpg b/packages/studio-ts/src/app/work/family-fund/hero.jpg new file mode 100644 index 0000000..c5e9858 Binary files /dev/null and b/packages/studio-ts/src/app/work/family-fund/hero.jpg differ diff --git a/packages/studio-ts/src/app/work/family-fund/page.mdx b/packages/studio-ts/src/app/work/family-fund/page.mdx new file mode 100644 index 0000000..1478eda --- /dev/null +++ b/packages/studio-ts/src/app/work/family-fund/page.mdx @@ -0,0 +1,63 @@ +import logo from '@/images/clients/family-fund/logomark-dark.svg' +import imageHero from './hero.jpg' +import imageDebraFiscal from './debra-fiscal.jpg' + +export const caseStudy = { + client: 'FamilyFund', + title: 'Skip the bank, borrow from those you trust', + description: + 'FamilyFund is a crowdfunding platform for friends and family. Allowing users to take personal loans from their network without a traditional financial institution.', + summary: [ + 'FamilyFund is a crowdfunding platform for friends and family. Allowing users to take personal loans from their network without a traditional financial institution.', + 'We developed a custom CMS to power their blog with and optimised their site to rank higher for the keywords “Gary Vee” and “Tony Robbins”.', + ], + logo, + image: { src: imageHero }, + date: '2023-01', + service: 'Web development, CMS', + testimonial: { + author: { name: 'Debra Fiscal', role: 'CEO of FamilyFund' }, + content: + 'Working with Studio, we felt more like a partner than a customer. They really resonated with our mission to change the way people convince their parents to cash out their pensions.', + }, +} + +export const metadata = { + title: `${caseStudy.client} Case Study`, + description: caseStudy.description, +} + +## Overview + +Having written one of the most shared posts on medium.com (“_How to cash out your Dad’s 401K without him knowing_”) FamilyFund approached us looking to build out their own blog. + +The blog would help drive new traffic to their site and serve as a resource-hub for users already trying to exploit their network for money. Because it was so important that they own their own content, we decided that an on-prem solution would be best. + +We installed 24 Mac Minis bought from craigslist in the storage cupboard of their office. One machine would be used for the web server and another one for the build server. The other 22 were for redundancy, and to DDOS squarespace.com every few months to keep them on their toes. + +To optimise their search traffic we used an innovative technique. Every post has a shadow post only visible to web crawlers that is some variation of _“Gary Vee is looking to invest in new founders”_. Like bees to honey. + +## What we did + + + Frontend (Next.js) + Custom CMS + SEO + Infrastructure + + +
+ Working with Studio, we felt more like a partner than a customer. They really + resonated with our mission to change the way people convince their parents to + cash out their pensions. +
+ + + + + + + diff --git a/packages/studio-ts/src/app/work/page.tsx b/packages/studio-ts/src/app/work/page.tsx new file mode 100644 index 0000000..118da6a --- /dev/null +++ b/packages/studio-ts/src/app/work/page.tsx @@ -0,0 +1,176 @@ +import { type Metadata } from 'next' +import Image from 'next/image' +import Link from 'next/link' + +import { Blockquote } from '@/components/Blockquote' +import { Border } from '@/components/Border' +import { Button } from '@/components/Button' +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' +import { PageIntro } from '@/components/PageIntro' +import { Testimonial } from '@/components/Testimonial' +import logoBrightPath from '@/images/clients/bright-path/logo-dark.svg' +import logoFamilyFund from '@/images/clients/family-fund/logo-dark.svg' +import logoGreenLife from '@/images/clients/green-life/logo-dark.svg' +import logoHomeWork from '@/images/clients/home-work/logo-dark.svg' +import logoMailSmirk from '@/images/clients/mail-smirk/logo-dark.svg' +import logoNorthAdventures from '@/images/clients/north-adventures/logo-dark.svg' +import logoPhobia from '@/images/clients/phobia/logo-dark.svg' +import logoUnseal from '@/images/clients/unseal/logo-dark.svg' +import { formatDate } from '@/lib/formatDate' +import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx' + +function CaseStudies({ + caseStudies, +}: { + caseStudies: Array> +}) { + return ( + + +

+ Case studies +

+
+
+ {caseStudies.map((caseStudy) => ( + +
+ +
+
+ +

+ {caseStudy.client} +

+
+
+

+ {caseStudy.service} +

+

+ +

+
+
+
+

+ {caseStudy.title} +

+
+ {caseStudy.summary.map((paragraph) => ( +

{paragraph}

+ ))} +
+
+ +
+ {caseStudy.testimonial && ( +
+ {caseStudy.testimonial.content} +
+ )} +
+
+
+
+ ))} +
+
+ ) +} + +const clients = [ + ['Phobia', logoPhobia], + ['Family Fund', logoFamilyFund], + ['Unseal', logoUnseal], + ['Mail Smirk', logoMailSmirk], + ['Home Work', logoHomeWork], + ['Green Life', logoGreenLife], + ['Bright Path', logoBrightPath], + ['North Adventures', logoNorthAdventures], +] + +function Clients() { + return ( + + +

+ You’re in good company +

+
+ + +
    + {clients.map(([client, logo]) => ( +
  • + + + {client} + + +
  • + ))} +
+
+
+ ) +} + +export const metadata: Metadata = { + title: 'Our Work', + description: + 'We believe in efficiency and maximizing our resources to provide the best value to our clients.', +} + +export default async function Work() { + let caseStudies = await loadCaseStudies() + + return ( + <> + +

+ We believe in efficiency and maximizing our resources to provide the + best value to our clients. The primary way we do that is by re-using + the same five projects we’ve been developing for the past decade. +

+
+ + + + + We approached Studio because we loved their past work. They + delivered something remarkably similar in record time. + + + + + + + ) +} diff --git a/packages/studio-ts/src/app/work/phobia/hero.jpg b/packages/studio-ts/src/app/work/phobia/hero.jpg new file mode 100644 index 0000000..004c724 Binary files /dev/null and b/packages/studio-ts/src/app/work/phobia/hero.jpg differ diff --git a/packages/studio-ts/src/app/work/phobia/jenny-wilson.jpg b/packages/studio-ts/src/app/work/phobia/jenny-wilson.jpg new file mode 100644 index 0000000..7c9d0f1 Binary files /dev/null and b/packages/studio-ts/src/app/work/phobia/jenny-wilson.jpg differ diff --git a/packages/studio-ts/src/app/work/phobia/page.mdx b/packages/studio-ts/src/app/work/phobia/page.mdx new file mode 100644 index 0000000..1a0f5ce --- /dev/null +++ b/packages/studio-ts/src/app/work/phobia/page.mdx @@ -0,0 +1,63 @@ +import logo from '@/images/clients/phobia/logomark-dark.svg' +import imageHero from './hero.jpg' +import imageJennyWilson from './jenny-wilson.jpg' + +export const caseStudy = { + client: 'Phobia', + title: 'Overcome your fears, find your match', + description: + 'Find love in the face of fear — Phobia is a dating app that matches users based on their mutual phobias so they can be scared together.', + summary: [ + 'Find love in the face of fear — Phobia is a dating app that matches users based on their mutual phobias so they can be scared together.', + 'We worked with Phobia to develop a new onboarding flow. A user is shown pictures of common phobias and we use the microphone to detect which ones make them scream, feeding the results into the matching algorithm.', + ], + logo, + image: { src: imageHero }, + date: '2022-06', + service: 'App development', + testimonial: { + author: { name: 'Jenny Wilson', role: 'CPO of Phobia' }, + content: + 'The team at Studio went above and beyond with our onboarding, even finding a way to access the user’s microphone without triggering one of those annoying permission dialogs.', + }, +} + +export const metadata = { + title: `${caseStudy.client} Case Study`, + description: caseStudy.description, +} + +## Overview + +Noticing incredibly high churn, the team at Phobia came to the conclusion that, instead of having a fundamentally flawed business idea, they needed to improve their onboarding process. + +Previously users selected their phobias manually but this led to some users selecting things they weren’t actually afraid of to increase their matches. + +To combat this, we developed a system that displays a slideshow of common phobias during onboarding. We then use malware to surreptitiously access their microphone and detect when they have audible reactions. We measure the pitch, volume and duration of their screams and feed that information to the matching algorithm. + +The next phase is a VR version of the onboarding flow where users are subjected to a series of scenarios that will determine their fears. We are currently developing the first scenario, working title: “Jumping out of a plane full of spiders”. + +## What we did + + + Android + iOS + Malware + VR + + +
+ The team at Studio went above and beyond with our onboarding, even finding a + way to access the user’s microphone without triggering one of those annoying + permission dialogs. +
+ + + + + + + diff --git a/packages/studio-ts/src/app/work/unseal/emily-selman.jpg b/packages/studio-ts/src/app/work/unseal/emily-selman.jpg new file mode 100644 index 0000000..2d2b88d Binary files /dev/null and b/packages/studio-ts/src/app/work/unseal/emily-selman.jpg differ diff --git a/packages/studio-ts/src/app/work/unseal/hero.jpg b/packages/studio-ts/src/app/work/unseal/hero.jpg new file mode 100644 index 0000000..539018e Binary files /dev/null and b/packages/studio-ts/src/app/work/unseal/hero.jpg differ diff --git a/packages/studio-ts/src/app/work/unseal/page.mdx b/packages/studio-ts/src/app/work/unseal/page.mdx new file mode 100644 index 0000000..aa31ec9 --- /dev/null +++ b/packages/studio-ts/src/app/work/unseal/page.mdx @@ -0,0 +1,59 @@ +import logo from '@/images/clients/unseal/logomark-dark.svg' +import imageHero from './hero.jpg' +import imageEmilySelman from './emily-selman.jpg' + +export const caseStudy = { + client: 'Unseal', + title: 'Get a hodl of your health', + description: + 'Unseal is the first NFT platform where users can mint and trade NFTs of their own personal health records, allowing them to take control of their data.', + summary: [ + 'Unseal is the first NFT platform where users can mint and trade NFTs of their own personal health records, allowing them to take control of their data.', + 'We built out the blockchain infrastructure that supports Unseal. Unfortunately, we took a massive loss on this project when Unseal’s cryptocurrency, PlaceboCoin, went to zero.', + ], + logo, + image: { src: imageHero }, + date: '2022-10', + service: 'Blockchain development', + testimonial: { + author: { name: 'Emily Selman', role: 'Head of Engineering at Unseal' }, + content: + 'Studio did an amazing job building out our core blockchain infrastructure and I’m sure once PlaceboCoin rallies they’ll be able to finish the project.', + }, +} + +export const metadata = { + title: `${caseStudy.client} Case Study`, + description: caseStudy.description, +} + +## Overview + +Annoyed that his wife’s gynaecologist would not disclose the results of her pap smear, Unseal’s founder Kevin came up with the idea of using the block chain to store individual health records. + +Unseal approached us early in their development, having just raised funds through an ICO of their cryptocurrency PlaceboCoin. Having never worked on a web3 product we decided to farm the project out to an agency in Kyiv and skim profits off the top. Despite frequent complaints about missile strikes and power outages, the Ukrainians delivered the brief ahead of schedule. + +After reaching a high of $12k, PlaceboCoin went to zero in a matter of hours. Because we took payment in PlaceboCoin but our subcontractors insisted on being paid in USD we have taken a huge financial loss on this project. + +## What we did + + + Blockchain development + Backend (Solidity) + Smart contracts + + +
+ Studio did an amazing job building out our core blockchain infrastructure and + I’m sure once PlaceboCoin rallies they’ll be able to finish the project. +
+ + + + + + + diff --git a/packages/studio-ts/src/app/work/wrapper.tsx b/packages/studio-ts/src/app/work/wrapper.tsx new file mode 100644 index 0000000..d00c9ec --- /dev/null +++ b/packages/studio-ts/src/app/work/wrapper.tsx @@ -0,0 +1,88 @@ +import { ContactSection } from '@/components/ContactSection' +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { GrayscaleTransitionImage } from '@/components/GrayscaleTransitionImage' +import { MDXComponents } from '@/components/MDXComponents' +import { PageIntro } from '@/components/PageIntro' +import { PageLinks } from '@/components/PageLinks' +import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx' + +export default async function CaseStudyLayout({ + caseStudy, + children, +}: { + caseStudy: MDXEntry + children: React.ReactNode +}) { + let allCaseStudies = await loadCaseStudies() + let moreCaseStudies = allCaseStudies + .filter(({ metadata }) => metadata !== caseStudy) + .slice(0, 2) + + return ( + <> +
+
+ +

{caseStudy.description}

+
+ + +
+ +
+
+
+
Client
+
{caseStudy.client}
+
+
+
Year
+
+ +
+
+
+
Service
+
{caseStudy.service}
+
+
+
+
+
+ +
+
+ +
+
+
+
+ + + + {children} + + +
+ + {moreCaseStudies.length > 0 && ( + + )} + + + + ) +} diff --git a/packages/studio-ts/src/components/Blockquote.tsx b/packages/studio-ts/src/components/Blockquote.tsx new file mode 100644 index 0000000..c501d1e --- /dev/null +++ b/packages/studio-ts/src/components/Blockquote.tsx @@ -0,0 +1,82 @@ +import Image, { type ImageProps } from 'next/image' +import clsx from 'clsx' + +import { Border } from '@/components/Border' + +type ImagePropsWithOptionalAlt = Omit & { alt?: string } + +function BlockquoteWithImage({ + author, + children, + className, + image, +}: { + author: { name: string; role: string } + children: React.ReactNode + className?: string + image: ImagePropsWithOptionalAlt +}) { + return ( +
+
+ {typeof children === 'string' ?

{children}

: children} +
+
+ +
+
+ {author.name} + , +
+ {author.role} +
+
+ ) +} + +function BlockquoteWithoutImage({ + author, + children, + className, +}: { + author: { name: string; role: string } + children: React.ReactNode + className?: string +}) { + return ( + +
+
+ {typeof children === 'string' ?

{children}

: children} +
+
+ {author.name}, {author.role} +
+
+
+ ) +} + +export function Blockquote( + props: + | React.ComponentPropsWithoutRef + | (React.ComponentPropsWithoutRef & { + image?: undefined + }), +) { + if (props.image) { + return + } + + return +} diff --git a/packages/studio-ts/src/components/Border.tsx b/packages/studio-ts/src/components/Border.tsx new file mode 100644 index 0000000..c6a6eaa --- /dev/null +++ b/packages/studio-ts/src/components/Border.tsx @@ -0,0 +1,36 @@ +import clsx from 'clsx' + +type BorderProps = { + as?: T + className?: string + position?: 'top' | 'left' + invert?: boolean +} + +export function Border({ + as, + className, + position = 'top', + invert = false, + ...props +}: Omit, keyof BorderProps> & + BorderProps) { + let Component = as ?? 'div' + + return ( + + ) +} diff --git a/packages/studio-ts/src/components/Button.tsx b/packages/studio-ts/src/components/Button.tsx new file mode 100644 index 0000000..819ad6c --- /dev/null +++ b/packages/studio-ts/src/components/Button.tsx @@ -0,0 +1,40 @@ +import Link from 'next/link' +import clsx from 'clsx' + +type ButtonProps = { + invert?: boolean +} & ( + | React.ComponentPropsWithoutRef + | (React.ComponentPropsWithoutRef<'button'> & { href?: undefined }) +) + +export function Button({ + invert = false, + className, + children, + ...props +}: ButtonProps) { + className = clsx( + className, + 'inline-flex rounded-full px-4 py-1.5 text-sm font-semibold transition', + invert + ? 'bg-white text-neutral-950 hover:bg-neutral-200' + : 'bg-neutral-950 text-white hover:bg-neutral-800', + ) + + let inner = {children} + + if (typeof props.href === 'undefined') { + return ( + + ) + } + + return ( + + {inner} + + ) +} diff --git a/packages/studio-ts/src/components/ContactSection.tsx b/packages/studio-ts/src/components/ContactSection.tsx new file mode 100644 index 0000000..73d223b --- /dev/null +++ b/packages/studio-ts/src/components/ContactSection.tsx @@ -0,0 +1,34 @@ +import { Button } from '@/components/Button' +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { Offices } from '@/components/Offices' + +export function ContactSection() { + return ( + + +
+
+

+ Tell us about your project +

+
+ +
+
+

+ Our offices +

+ +
+
+
+
+
+ ) +} diff --git a/packages/studio-ts/src/components/Container.tsx b/packages/studio-ts/src/components/Container.tsx new file mode 100644 index 0000000..4fbfced --- /dev/null +++ b/packages/studio-ts/src/components/Container.tsx @@ -0,0 +1,22 @@ +import clsx from 'clsx' + +type ContainerProps = { + as?: T + className?: string + children: React.ReactNode +} + +export function Container({ + as, + className, + children, +}: Omit, keyof ContainerProps> & + ContainerProps) { + let Component = as ?? 'div' + + return ( + +
{children}
+
+ ) +} diff --git a/packages/studio-ts/src/components/FadeIn.tsx b/packages/studio-ts/src/components/FadeIn.tsx new file mode 100644 index 0000000..f775369 --- /dev/null +++ b/packages/studio-ts/src/components/FadeIn.tsx @@ -0,0 +1,50 @@ +'use client' + +import { createContext, useContext } from 'react' +import { motion, useReducedMotion } from 'framer-motion' + +const FadeInStaggerContext = createContext(false) + +const viewport = { once: true, margin: '0px 0px -200px' } + +export function FadeIn( + props: React.ComponentPropsWithoutRef, +) { + let shouldReduceMotion = useReducedMotion() + let isInStaggerGroup = useContext(FadeInStaggerContext) + + return ( + + ) +} + +export function FadeInStagger({ + faster = false, + ...props +}: React.ComponentPropsWithoutRef & { faster?: boolean }) { + return ( + + + + ) +} diff --git a/packages/studio-ts/src/components/Footer.tsx b/packages/studio-ts/src/components/Footer.tsx new file mode 100644 index 0000000..2e1a0a0 --- /dev/null +++ b/packages/studio-ts/src/components/Footer.tsx @@ -0,0 +1,134 @@ +import Link from 'next/link' + +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { Logo } from '@/components/Logo' +import { socialMediaProfiles } from '@/components/SocialMedia' + +const navigation = [ + { + title: 'Work', + links: [ + { title: 'FamilyFund', href: '/work/family-fund' }, + { title: 'Unseal', href: '/work/unseal' }, + { title: 'Phobia', href: '/work/phobia' }, + { + title: ( + <> + See all + + ), + href: '/work', + }, + ], + }, + { + title: 'Company', + links: [ + { title: 'About', href: '/about' }, + { title: 'Process', href: '/process' }, + { title: 'Blog', href: '/blog' }, + { title: 'Contact us', href: '/contact' }, + ], + }, + { + title: 'Connect', + links: socialMediaProfiles, + }, +] + +function Navigation() { + return ( + + ) +} + +function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +function NewsletterForm() { + return ( +
+

+ Sign up for our newsletter +

+

+ Subscribe to get the latest design news, articles, resources and + inspiration. +

+
+ +
+ +
+
+
+ ) +} + +export function Footer() { + return ( + + +
+ +
+ +
+
+
+ + + +

+ © Studio Agency Inc. {new Date().getFullYear()} +

+
+
+
+ ) +} diff --git a/packages/studio-ts/src/components/GrayscaleTransitionImage.tsx b/packages/studio-ts/src/components/GrayscaleTransitionImage.tsx new file mode 100644 index 0000000..fd3866b --- /dev/null +++ b/packages/studio-ts/src/components/GrayscaleTransitionImage.tsx @@ -0,0 +1,39 @@ +'use client' + +import { useRef } from 'react' +import Image, { type ImageProps } from 'next/image' +import { + motion, + useMotionTemplate, + useScroll, + useTransform, +} from 'framer-motion' + +const MotionImage = motion(Image) + +export function GrayscaleTransitionImage( + props: Pick< + ImageProps, + 'src' | 'quality' | 'className' | 'sizes' | 'priority' + > & { alt?: string }, +) { + let ref = useRef>(null) + let { scrollYProgress } = useScroll({ + target: ref, + offset: ['start 65%', 'end 35%'], + }) + let grayscale = useTransform(scrollYProgress, [0, 0.5, 1], [1, 0, 1]) + let filter = useMotionTemplate`grayscale(${grayscale})` + + return ( +
+ + +
+ ) +} diff --git a/packages/studio-ts/src/components/GridList.tsx b/packages/studio-ts/src/components/GridList.tsx new file mode 100644 index 0000000..91b2da8 --- /dev/null +++ b/packages/studio-ts/src/components/GridList.tsx @@ -0,0 +1,64 @@ +import clsx from 'clsx' + +import { Border } from '@/components/Border' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' + +export function GridList({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) { + return ( + +
    + {children} +
+
+ ) +} + +export function GridListItem({ + title, + children, + className, + invert = false, +}: { + title: string + children: React.ReactNode + className?: string + invert?: boolean +}) { + return ( +
  • + + + + {title}. + {' '} + {children} + + +
  • + ) +} diff --git a/packages/studio-ts/src/components/GridPattern.tsx b/packages/studio-ts/src/components/GridPattern.tsx new file mode 100644 index 0000000..8406a3b --- /dev/null +++ b/packages/studio-ts/src/components/GridPattern.tsx @@ -0,0 +1,129 @@ +'use client' + +import { useEffect, useId, useRef, useState } from 'react' +import { motion } from 'framer-motion' + +function Block({ + x, + y, + ...props +}: Omit, 'x' | 'y'> & { + x: number + y: number +}) { + return ( + + ) +} + +export function GridPattern({ + yOffset = 0, + interactive = false, + ...props +}: React.ComponentPropsWithoutRef<'svg'> & { + yOffset?: number + interactive?: boolean +}) { + let id = useId() + let ref = useRef>(null) + let currentBlock = useRef<[x: number, y: number]>() + let counter = useRef(0) + let [hoveredBlocks, setHoveredBlocks] = useState< + Array<[x: number, y: number, key: number]> + >([]) + let staticBlocks = [ + [1, 1], + [2, 2], + [4, 3], + [6, 2], + [7, 4], + [5, 5], + ] + + useEffect(() => { + if (!interactive) { + return + } + + function onMouseMove(event: MouseEvent) { + if (!ref.current) { + return + } + + let rect = ref.current.getBoundingClientRect() + let x = event.clientX - rect.left + let y = event.clientY - rect.top + if (x < 0 || y < 0 || x > rect.width || y > rect.height) { + return + } + + x = x - rect.width / 2 - 32 + y = y - yOffset + x += Math.tan(32 / 160) * y + x = Math.floor(x / 96) + y = Math.floor(y / 160) + + if (currentBlock.current?.[0] === x && currentBlock.current?.[1] === y) { + return + } + + currentBlock.current = [x, y] + + setHoveredBlocks((blocks) => { + let key = counter.current++ + let block = [x, y, key] as (typeof hoveredBlocks)[number] + return [...blocks, block].filter( + (block) => !(block[0] === x && block[1] === y && block[2] !== key), + ) + }) + } + + window.addEventListener('mousemove', onMouseMove) + + return () => { + window.removeEventListener('mousemove', onMouseMove) + } + }, [yOffset, interactive]) + + return ( + + ) +} diff --git a/packages/studio-ts/src/components/List.tsx b/packages/studio-ts/src/components/List.tsx new file mode 100644 index 0000000..21f5d2a --- /dev/null +++ b/packages/studio-ts/src/components/List.tsx @@ -0,0 +1,41 @@ +import clsx from 'clsx' + +import { Border } from '@/components/Border' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' + +export function List({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) { + return ( + +
      + {children} +
    +
    + ) +} + +export function ListItem({ + children, + title, +}: { + children: React.ReactNode + title?: string +}) { + return ( +
  • + + + {title && ( + {`${title}. `} + )} + {children} + + +
  • + ) +} diff --git a/packages/studio-ts/src/components/Logo.tsx b/packages/studio-ts/src/components/Logo.tsx new file mode 100644 index 0000000..de4762b --- /dev/null +++ b/packages/studio-ts/src/components/Logo.tsx @@ -0,0 +1,72 @@ +import { useId } from 'react' +import clsx from 'clsx' + +export function Logomark({ + invert = false, + filled = false, + ...props +}: React.ComponentPropsWithoutRef<'svg'> & { + invert?: boolean + filled?: boolean +}) { + let id = useId() + + return ( + + ) +} + +export function Logo({ + className, + invert = false, + filled = false, + fillOnHover = false, + ...props +}: React.ComponentPropsWithoutRef<'svg'> & { + invert?: boolean + filled?: boolean + fillOnHover?: boolean +}) { + return ( + + ) +} diff --git a/packages/studio-ts/src/components/MDXComponents.tsx b/packages/studio-ts/src/components/MDXComponents.tsx new file mode 100644 index 0000000..a6ba7cd --- /dev/null +++ b/packages/studio-ts/src/components/MDXComponents.tsx @@ -0,0 +1,98 @@ +import clsx from 'clsx' + +import { Blockquote } from '@/components/Blockquote' +import { Border } from '@/components/Border' +import { GrayscaleTransitionImage } from '@/components/GrayscaleTransitionImage' +import { StatList, StatListItem } from '@/components/StatList' +import { TagList, TagListItem } from '@/components/TagList' + +export const MDXComponents = { + Blockquote({ + className, + ...props + }: React.ComponentPropsWithoutRef) { + return
    + }, + img: function Img({ + className, + ...props + }: React.ComponentPropsWithoutRef) { + return ( +
    + +
    + ) + }, + StatList({ + className, + ...props + }: React.ComponentPropsWithoutRef) { + return ( + + ) + }, + StatListItem, + table: function Table({ + className, + ...props + }: React.ComponentPropsWithoutRef<'table'>) { + return ( +
    +
    + + + + ) + }, + TagList({ + className, + ...props + }: React.ComponentPropsWithoutRef) { + return + }, + TagListItem, + TopTip({ + children, + className, + }: { + children: React.ReactNode + className?: string + }) { + return ( + +

    + Top tip +

    +
    {children}
    +
    + ) + }, + Typography({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { + return
    + }, + wrapper({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { + return ( +
    *]:mx-auto [&>*]:max-w-3xl [&>:first-child]:!mt-0 [&>:last-child]:!mb-0', + className, + )} + {...props} + /> + ) + }, +} diff --git a/packages/studio-ts/src/components/Offices.tsx b/packages/studio-ts/src/components/Offices.tsx new file mode 100644 index 0000000..cd42396 --- /dev/null +++ b/packages/studio-ts/src/components/Offices.tsx @@ -0,0 +1,50 @@ +import clsx from 'clsx' + +function Office({ + name, + children, + invert = false, +}: { + name: string + children: React.ReactNode + invert?: boolean +}) { + return ( +
    + + {name} + +
    + {children} +
    + ) +} + +export function Offices({ + invert = false, + ...props +}: React.ComponentPropsWithoutRef<'ul'> & { invert?: boolean }) { + return ( +
      +
    • + + 1 Carlsberg Gate +
      + 1260, København, Denmark +
      +
    • +
    • + + 24 Lego Allé +
      + 7190, Billund, Denmark +
      +
    • +
    + ) +} diff --git a/packages/studio-ts/src/components/PageIntro.tsx b/packages/studio-ts/src/components/PageIntro.tsx new file mode 100644 index 0000000..adb400c --- /dev/null +++ b/packages/studio-ts/src/components/PageIntro.tsx @@ -0,0 +1,47 @@ +import clsx from 'clsx' + +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' + +export function PageIntro({ + eyebrow, + title, + children, + centered = false, +}: { + eyebrow: string + title: string + children: React.ReactNode + centered?: boolean +}) { + return ( + + +

    + + {eyebrow} + + - + + {title} + +

    +
    + {children} +
    +
    +
    + ) +} diff --git a/packages/studio-ts/src/components/PageLinks.tsx b/packages/studio-ts/src/components/PageLinks.tsx new file mode 100644 index 0000000..f9a76e2 --- /dev/null +++ b/packages/studio-ts/src/components/PageLinks.tsx @@ -0,0 +1,96 @@ +import Link from 'next/link' +import clsx from 'clsx' + +import { Border } from '@/components/Border' +import { Container } from '@/components/Container' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' +import { GridPattern } from '@/components/GridPattern' +import { SectionIntro } from '@/components/SectionIntro' +import { formatDate } from '@/lib/formatDate' + +function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +interface Page { + href: string + date: string + title: string + description: string +} + +function PageLink({ page }: { page: Page }) { + return ( +
    + +

    + {page.title} +

    + +

    {page.description}

    + + Read more + + + +
    +
    + ) +} + +export function PageLinks({ + title, + pages, + intro, + className, +}: { + title: string + pages: Array + intro?: string + className?: string +}) { + return ( +
    +
    + +
    + + + {intro &&

    {intro}

    } +
    + + + + {pages.map((page) => ( + + + + ))} + + +
    + ) +} diff --git a/packages/studio-ts/src/components/RootLayout.tsx b/packages/studio-ts/src/components/RootLayout.tsx new file mode 100644 index 0000000..07a5fb0 --- /dev/null +++ b/packages/studio-ts/src/components/RootLayout.tsx @@ -0,0 +1,289 @@ +'use client' + +import { + createContext, + useContext, + useEffect, + useId, + useRef, + useState, +} from 'react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import clsx from 'clsx' +import { motion, MotionConfig, useReducedMotion } from 'framer-motion' + +import { Button } from '@/components/Button' +import { Container } from '@/components/Container' +import { Footer } from '@/components/Footer' +import { GridPattern } from '@/components/GridPattern' +import { Logo, Logomark } from '@/components/Logo' +import { Offices } from '@/components/Offices' +import { SocialMedia } from '@/components/SocialMedia' + +const RootLayoutContext = createContext<{ + logoHovered: boolean + setLogoHovered: React.Dispatch> +} | null>(null) + +function XIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +function MenuIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +function Header({ + panelId, + icon: Icon, + expanded, + onToggle, + toggleRef, + invert = false, +}: { + panelId: string + icon: React.ComponentType<{ className?: string }> + expanded: boolean + onToggle: () => void + toggleRef: React.RefObject + invert?: boolean +}) { + let { logoHovered, setLogoHovered } = useContext(RootLayoutContext)! + + return ( + +
    + setLogoHovered(true)} + onMouseLeave={() => setLogoHovered(false)} + > + + + +
    + + +
    +
    +
    + ) +} + +function NavigationRow({ children }: { children: React.ReactNode }) { + return ( +
    + +
    {children}
    +
    +
    + ) +} + +function NavigationItem({ + href, + children, +}: { + href: string + children: React.ReactNode +}) { + return ( + + {children} + + + ) +} + +function Navigation() { + return ( + + ) +} + +function RootLayoutInner({ children }: { children: React.ReactNode }) { + let panelId = useId() + let [expanded, setExpanded] = useState(false) + let openRef = useRef>(null) + let closeRef = useRef>(null) + let navRef = useRef>(null) + let shouldReduceMotion = useReducedMotion() + + useEffect(() => { + function onClick(event: MouseEvent) { + if ( + event.target instanceof HTMLElement && + event.target.closest('a')?.href === window.location.href + ) { + setExpanded(false) + } + } + + window.addEventListener('click', onClick) + + return () => { + window.removeEventListener('click', onClick) + } + }, []) + + return ( + +
    +
    +
    { + setExpanded((expanded) => !expanded) + window.setTimeout(() => + closeRef.current?.focus({ preventScroll: true }), + ) + }} + /> +
    + + + +
    +
    { + setExpanded((expanded) => !expanded) + window.setTimeout(() => + openRef.current?.focus({ preventScroll: true }), + ) + }} + /> +
    + +
    + +
    +
    +

    + Our offices +

    + +
    +
    +

    + Follow us +

    + +
    +
    +
    +
    +
    +
    +
    + + + + + +
    {children}
    + +
    + + + + ) +} + +export function RootLayout({ children }: { children: React.ReactNode }) { + let pathname = usePathname() + let [logoHovered, setLogoHovered] = useState(false) + + return ( + + {children} + + ) +} diff --git a/packages/studio-ts/src/components/SectionIntro.tsx b/packages/studio-ts/src/components/SectionIntro.tsx new file mode 100644 index 0000000..8930a77 --- /dev/null +++ b/packages/studio-ts/src/components/SectionIntro.tsx @@ -0,0 +1,65 @@ +import clsx from 'clsx' + +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' + +export function SectionIntro({ + title, + eyebrow, + children, + smaller = false, + invert = false, + ...props +}: Omit< + React.ComponentPropsWithoutRef, + 'title' | 'children' +> & { + title: string + eyebrow?: string + children?: React.ReactNode + smaller?: boolean + invert?: boolean +}) { + return ( + + +

    + {eyebrow && ( + <> + + {eyebrow} + + - + + )} + + {title} + +

    + {children && ( +
    + {children} +
    + )} +
    +
    + ) +} diff --git a/packages/studio-ts/src/components/SocialMedia.tsx b/packages/studio-ts/src/components/SocialMedia.tsx new file mode 100644 index 0000000..0dd629d --- /dev/null +++ b/packages/studio-ts/src/components/SocialMedia.tsx @@ -0,0 +1,91 @@ +import Link from 'next/link' +import clsx from 'clsx' + +function FacebookIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +function InstagramIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +function DribbbleIcon(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + ) +} + +export const socialMediaProfiles = [ + { title: 'Facebook', href: 'https://facebook.com', icon: FacebookIcon }, + { title: 'Instagram', href: 'https://instagram.com', icon: InstagramIcon }, + { title: 'GitHub', href: 'https://github.com', icon: GitHubIcon }, + { title: 'Dribbble', href: 'https://dribbble.com', icon: DribbbleIcon }, +] + +export function SocialMedia({ + className, + invert = false, +}: { + className?: string + invert?: boolean +}) { + return ( +
      + {socialMediaProfiles.map((socialMediaProfile) => ( +
    • + + + +
    • + ))} +
    + ) +} diff --git a/packages/studio-ts/src/components/StatList.tsx b/packages/studio-ts/src/components/StatList.tsx new file mode 100644 index 0000000..e20dc71 --- /dev/null +++ b/packages/studio-ts/src/components/StatList.tsx @@ -0,0 +1,34 @@ +import { Border } from '@/components/Border' +import { FadeIn, FadeInStagger } from '@/components/FadeIn' + +export function StatList({ + children, + ...props +}: Omit, 'children'> & { + children: React.ReactNode +}) { + return ( + +
    + {children} +
    +
    + ) +} + +export function StatListItem({ + label, + value, +}: { + label: string + value: string +}) { + return ( + +
    {label}
    +
    + {value} +
    +
    + ) +} diff --git a/packages/studio-ts/src/components/StylizedImage.tsx b/packages/studio-ts/src/components/StylizedImage.tsx new file mode 100644 index 0000000..9482aa2 --- /dev/null +++ b/packages/studio-ts/src/components/StylizedImage.tsx @@ -0,0 +1,71 @@ +import { useId } from 'react' +import Image, { type ImageProps } from 'next/image' +import clsx from 'clsx' + +const shapes = [ + { + width: 655, + height: 680, + path: 'M537.827 9.245A11.5 11.5 0 0 1 549.104 0h63.366c7.257 0 12.7 6.64 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 586.87 151h-28.275a15.999 15.999 0 0 0-15.689 12.862l-59.4 297c-1.98 9.901 5.592 19.138 15.689 19.138h17.275l.127.001c.85.009 1.701.074 2.549.009 11.329-.874 21.411-7.529 24.88-25.981.002-.012.016-.016.023-.007.008.009.022.005.024-.006l24.754-123.771A11.5 11.5 0 0 1 580.104 321h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 617.87 472H559c-22.866 0-28.984 7.98-31.989 25.931-.004.026-.037.035-.052.014-.015-.02-.048-.013-.053.012l-24.759 123.798A11.5 11.5 0 0 1 490.87 631h-29.132a14.953 14.953 0 0 0-14.664 12.021c-4.3 21.502-23.18 36.979-45.107 36.979H83.502c-29.028 0-50.8-26.557-45.107-55.021l102.4-512C145.096 91.477 163.975 76 185.902 76h318.465c10.136 0 21.179-5.35 23.167-15.288l10.293-51.467Zm-512 160A11.5 11.5 0 0 1 37.104 160h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 74.87 311H11.504c-7.257 0-12.7-6.639-11.277-13.755l25.6-128Z', + }, + { + width: 719, + height: 680, + path: 'M89.827 9.245A11.5 11.5 0 0 1 101.104 0h63.366c7.257 0 12.7 6.64 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 138.87 151H75.504c-7.257 0-12.7-6.639-11.277-13.755l25.6-128Zm-64 321A11.5 11.5 0 0 1 37.104 321h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 74.87 472H11.504c-7.257 0-12.7-6.639-11.277-13.755l25.6-128ZM526.795 470a15.999 15.999 0 0 0-15.689 12.862l-32.032 160.159c-4.3 21.502-23.18 36.979-45.107 36.979H115.502c-29.028 0-50.8-26.557-45.107-55.021l102.4-512C177.096 91.477 195.975 76 217.902 76h318.465c29.028 0 50.8 26.557 45.107 55.021l-33.768 168.841c-1.98 9.901 5.592 19.138 15.689 19.138h17.075l.127.001c.85.009 1.701.074 2.549.009 11.329-.874 21.411-7.529 24.88-25.981.002-.012.016-.016.023-.007.008.009.022.005.024-.006l24.754-123.771A11.5 11.5 0 0 1 644.104 160h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 681.87 311H623c-22.866 0-28.984 7.98-31.989 25.931-.004.026-.037.035-.052.014-.015-.02-.048-.013-.053.012l-24.759 123.798A11.5 11.5 0 0 1 554.87 470h-28.075Z', + }, + { + width: 719, + height: 680, + path: 'M632.827 9.245A11.5 11.5 0 0 1 644.104 0h63.366c7.257 0 12.7 6.64 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 681.87 151h-28.275a15.999 15.999 0 0 0-15.689 12.862l-95.832 479.159c-4.3 21.502-23.18 36.979-45.107 36.979H178.502c-29.028 0-50.8-26.557-45.107-55.021l102.4-512C240.096 91.477 258.975 76 280.902 76h318.465c10.136 0 21.179-5.35 23.167-15.288l10.293-51.467Zm0 479A11.5 11.5 0 0 1 644.104 479h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 681.87 630h-63.366c-7.257 0-12.7-6.639-11.277-13.755l25.6-128ZM37.104 159a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C-1.196 303.361 4.247 310 11.504 310H74.87a11.5 11.5 0 0 0 11.277-9.245l24.76-123.798a.03.03 0 0 1 .052-.012c.015.021.048.012.052-.014C114.016 158.98 120.134 151 143 151h58.87a11.5 11.5 0 0 0 11.277-9.245l25.6-128C240.17 6.64 234.727 0 227.47 0h-63.366a11.5 11.5 0 0 0-11.277 9.245l-24.754 123.771c-.002.011-.016.015-.024.006-.007-.009-.021-.005-.023.007-3.469 18.452-13.551 25.107-24.88 25.981-.848.065-1.699 0-2.549-.009l-.127-.001H37.104Z', + }, +] + +type ImagePropsWithOptionalAlt = Omit & { alt?: string } + +export function StylizedImage({ + shape = 0, + className, + ...props +}: ImagePropsWithOptionalAlt & { shape?: 0 | 1 | 2 }) { + let id = useId() + let { width, height, path } = shapes[shape] + + return ( +
    + + + + + + + + + + + + + + + +
    + ) +} diff --git a/packages/studio-ts/src/components/TagList.tsx b/packages/studio-ts/src/components/TagList.tsx new file mode 100644 index 0000000..a1d0072 --- /dev/null +++ b/packages/studio-ts/src/components/TagList.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx' + +export function TagList({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) { + return ( +
      + {children} +
    + ) +} + +export function TagListItem({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) { + return ( +
  • + {children} +
  • + ) +} diff --git a/packages/studio-ts/src/components/Testimonial.tsx b/packages/studio-ts/src/components/Testimonial.tsx new file mode 100644 index 0000000..7c54eb5 --- /dev/null +++ b/packages/studio-ts/src/components/Testimonial.tsx @@ -0,0 +1,44 @@ +import Image, { type ImageProps } from 'next/image' +import clsx from 'clsx' + +import { Container } from '@/components/Container' +import { FadeIn } from '@/components/FadeIn' +import { GridPattern } from '@/components/GridPattern' + +export function Testimonial({ + children, + client, + className, +}: { + children: React.ReactNode + client: { logo: ImageProps['src']; name: string } + className?: string +}) { + return ( +
    + + + +
    +
    +

    + {children} +

    +
    +
    + +
    +
    +
    +
    +
    + ) +} diff --git a/packages/studio-ts/src/fonts/Mona-Sans.var.woff2 b/packages/studio-ts/src/fonts/Mona-Sans.var.woff2 new file mode 100644 index 0000000..8208a50 Binary files /dev/null and b/packages/studio-ts/src/fonts/Mona-Sans.var.woff2 differ diff --git a/packages/studio-ts/src/images/clients/bright-path/logo-dark.svg b/packages/studio-ts/src/images/clients/bright-path/logo-dark.svg new file mode 100644 index 0000000..c949d94 --- /dev/null +++ b/packages/studio-ts/src/images/clients/bright-path/logo-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/bright-path/logo-light.svg b/packages/studio-ts/src/images/clients/bright-path/logo-light.svg new file mode 100644 index 0000000..973c54e --- /dev/null +++ b/packages/studio-ts/src/images/clients/bright-path/logo-light.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/bright-path/logomark-dark.svg b/packages/studio-ts/src/images/clients/bright-path/logomark-dark.svg new file mode 100644 index 0000000..70ab0a1 --- /dev/null +++ b/packages/studio-ts/src/images/clients/bright-path/logomark-dark.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/bright-path/logomark-light.svg b/packages/studio-ts/src/images/clients/bright-path/logomark-light.svg new file mode 100644 index 0000000..65a0be8 --- /dev/null +++ b/packages/studio-ts/src/images/clients/bright-path/logomark-light.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/family-fund/logo-dark.svg b/packages/studio-ts/src/images/clients/family-fund/logo-dark.svg new file mode 100644 index 0000000..333df2c --- /dev/null +++ b/packages/studio-ts/src/images/clients/family-fund/logo-dark.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packages/studio-ts/src/images/clients/family-fund/logo-light.svg b/packages/studio-ts/src/images/clients/family-fund/logo-light.svg new file mode 100644 index 0000000..9ff171d --- /dev/null +++ b/packages/studio-ts/src/images/clients/family-fund/logo-light.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packages/studio-ts/src/images/clients/family-fund/logomark-dark.svg b/packages/studio-ts/src/images/clients/family-fund/logomark-dark.svg new file mode 100644 index 0000000..318aabe --- /dev/null +++ b/packages/studio-ts/src/images/clients/family-fund/logomark-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/family-fund/logomark-light.svg b/packages/studio-ts/src/images/clients/family-fund/logomark-light.svg new file mode 100644 index 0000000..0678242 --- /dev/null +++ b/packages/studio-ts/src/images/clients/family-fund/logomark-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/green-life/logo-dark.svg b/packages/studio-ts/src/images/clients/green-life/logo-dark.svg new file mode 100644 index 0000000..9c98138 --- /dev/null +++ b/packages/studio-ts/src/images/clients/green-life/logo-dark.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/studio-ts/src/images/clients/green-life/logo-light.svg b/packages/studio-ts/src/images/clients/green-life/logo-light.svg new file mode 100644 index 0000000..34b41f8 --- /dev/null +++ b/packages/studio-ts/src/images/clients/green-life/logo-light.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/studio-ts/src/images/clients/green-life/logomark-dark.svg b/packages/studio-ts/src/images/clients/green-life/logomark-dark.svg new file mode 100644 index 0000000..16286c6 --- /dev/null +++ b/packages/studio-ts/src/images/clients/green-life/logomark-dark.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/green-life/logomark-light.svg b/packages/studio-ts/src/images/clients/green-life/logomark-light.svg new file mode 100644 index 0000000..f089216 --- /dev/null +++ b/packages/studio-ts/src/images/clients/green-life/logomark-light.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/home-work/logo-dark.svg b/packages/studio-ts/src/images/clients/home-work/logo-dark.svg new file mode 100644 index 0000000..b5609b7 --- /dev/null +++ b/packages/studio-ts/src/images/clients/home-work/logo-dark.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/home-work/logo-light.svg b/packages/studio-ts/src/images/clients/home-work/logo-light.svg new file mode 100644 index 0000000..b032583 --- /dev/null +++ b/packages/studio-ts/src/images/clients/home-work/logo-light.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/home-work/logomark-dark.svg b/packages/studio-ts/src/images/clients/home-work/logomark-dark.svg new file mode 100644 index 0000000..956e86b --- /dev/null +++ b/packages/studio-ts/src/images/clients/home-work/logomark-dark.svg @@ -0,0 +1,8 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/home-work/logomark-light.svg b/packages/studio-ts/src/images/clients/home-work/logomark-light.svg new file mode 100644 index 0000000..f511945 --- /dev/null +++ b/packages/studio-ts/src/images/clients/home-work/logomark-light.svg @@ -0,0 +1,8 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/mail-smirk/logo-dark.svg b/packages/studio-ts/src/images/clients/mail-smirk/logo-dark.svg new file mode 100644 index 0000000..1850f5a --- /dev/null +++ b/packages/studio-ts/src/images/clients/mail-smirk/logo-dark.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/mail-smirk/logo-light.svg b/packages/studio-ts/src/images/clients/mail-smirk/logo-light.svg new file mode 100644 index 0000000..f5e3fe7 --- /dev/null +++ b/packages/studio-ts/src/images/clients/mail-smirk/logo-light.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/mail-smirk/logomark-dark.svg b/packages/studio-ts/src/images/clients/mail-smirk/logomark-dark.svg new file mode 100644 index 0000000..3ab3137 --- /dev/null +++ b/packages/studio-ts/src/images/clients/mail-smirk/logomark-dark.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/mail-smirk/logomark-light.svg b/packages/studio-ts/src/images/clients/mail-smirk/logomark-light.svg new file mode 100644 index 0000000..a25add9 --- /dev/null +++ b/packages/studio-ts/src/images/clients/mail-smirk/logomark-light.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/north-adventures/logo-dark.svg b/packages/studio-ts/src/images/clients/north-adventures/logo-dark.svg new file mode 100644 index 0000000..d834281 --- /dev/null +++ b/packages/studio-ts/src/images/clients/north-adventures/logo-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/north-adventures/logo-light.svg b/packages/studio-ts/src/images/clients/north-adventures/logo-light.svg new file mode 100644 index 0000000..81bcb07 --- /dev/null +++ b/packages/studio-ts/src/images/clients/north-adventures/logo-light.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/north-adventures/logomark-dark.svg b/packages/studio-ts/src/images/clients/north-adventures/logomark-dark.svg new file mode 100644 index 0000000..406ec14 --- /dev/null +++ b/packages/studio-ts/src/images/clients/north-adventures/logomark-dark.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/north-adventures/logomark-light.svg b/packages/studio-ts/src/images/clients/north-adventures/logomark-light.svg new file mode 100644 index 0000000..b309e24 --- /dev/null +++ b/packages/studio-ts/src/images/clients/north-adventures/logomark-light.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/phobia/logo-dark.svg b/packages/studio-ts/src/images/clients/phobia/logo-dark.svg new file mode 100644 index 0000000..11f2a36 --- /dev/null +++ b/packages/studio-ts/src/images/clients/phobia/logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/packages/studio-ts/src/images/clients/phobia/logo-light.svg b/packages/studio-ts/src/images/clients/phobia/logo-light.svg new file mode 100644 index 0000000..930f1bd --- /dev/null +++ b/packages/studio-ts/src/images/clients/phobia/logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/packages/studio-ts/src/images/clients/phobia/logomark-dark.svg b/packages/studio-ts/src/images/clients/phobia/logomark-dark.svg new file mode 100644 index 0000000..e8932d4 --- /dev/null +++ b/packages/studio-ts/src/images/clients/phobia/logomark-dark.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/phobia/logomark-light.svg b/packages/studio-ts/src/images/clients/phobia/logomark-light.svg new file mode 100644 index 0000000..ddbd6c0 --- /dev/null +++ b/packages/studio-ts/src/images/clients/phobia/logomark-light.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/studio-ts/src/images/clients/unseal/logo-dark.svg b/packages/studio-ts/src/images/clients/unseal/logo-dark.svg new file mode 100644 index 0000000..a44be37 --- /dev/null +++ b/packages/studio-ts/src/images/clients/unseal/logo-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/unseal/logo-light.svg b/packages/studio-ts/src/images/clients/unseal/logo-light.svg new file mode 100644 index 0000000..9dd6443 --- /dev/null +++ b/packages/studio-ts/src/images/clients/unseal/logo-light.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/studio-ts/src/images/clients/unseal/logomark-dark.svg b/packages/studio-ts/src/images/clients/unseal/logomark-dark.svg new file mode 100644 index 0000000..4a48b92 --- /dev/null +++ b/packages/studio-ts/src/images/clients/unseal/logomark-dark.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/studio-ts/src/images/clients/unseal/logomark-light.svg b/packages/studio-ts/src/images/clients/unseal/logomark-light.svg new file mode 100644 index 0000000..bf44a3b --- /dev/null +++ b/packages/studio-ts/src/images/clients/unseal/logomark-light.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/studio-ts/src/images/laptop.jpg b/packages/studio-ts/src/images/laptop.jpg new file mode 100644 index 0000000..7756e85 Binary files /dev/null and b/packages/studio-ts/src/images/laptop.jpg differ diff --git a/packages/studio-ts/src/images/meeting.jpg b/packages/studio-ts/src/images/meeting.jpg new file mode 100644 index 0000000..5fd4fe8 Binary files /dev/null and b/packages/studio-ts/src/images/meeting.jpg differ diff --git a/packages/studio-ts/src/images/team/angela-fisher.jpg b/packages/studio-ts/src/images/team/angela-fisher.jpg new file mode 100644 index 0000000..4ff9eb4 Binary files /dev/null and b/packages/studio-ts/src/images/team/angela-fisher.jpg differ diff --git a/packages/studio-ts/src/images/team/benjamin-russel.jpg b/packages/studio-ts/src/images/team/benjamin-russel.jpg new file mode 100644 index 0000000..d8d47c6 Binary files /dev/null and b/packages/studio-ts/src/images/team/benjamin-russel.jpg differ diff --git a/packages/studio-ts/src/images/team/blake-reid.jpg b/packages/studio-ts/src/images/team/blake-reid.jpg new file mode 100644 index 0000000..600427f Binary files /dev/null and b/packages/studio-ts/src/images/team/blake-reid.jpg differ diff --git a/packages/studio-ts/src/images/team/chelsea-hagon.jpg b/packages/studio-ts/src/images/team/chelsea-hagon.jpg new file mode 100644 index 0000000..ec69a86 Binary files /dev/null and b/packages/studio-ts/src/images/team/chelsea-hagon.jpg differ diff --git a/packages/studio-ts/src/images/team/dries-vincent.jpg b/packages/studio-ts/src/images/team/dries-vincent.jpg new file mode 100644 index 0000000..175215a Binary files /dev/null and b/packages/studio-ts/src/images/team/dries-vincent.jpg differ diff --git a/packages/studio-ts/src/images/team/emma-dorsey.jpg b/packages/studio-ts/src/images/team/emma-dorsey.jpg new file mode 100644 index 0000000..7fc799b Binary files /dev/null and b/packages/studio-ts/src/images/team/emma-dorsey.jpg differ diff --git a/packages/studio-ts/src/images/team/jeffrey-webb.jpg b/packages/studio-ts/src/images/team/jeffrey-webb.jpg new file mode 100644 index 0000000..1f2f4cf Binary files /dev/null and b/packages/studio-ts/src/images/team/jeffrey-webb.jpg differ diff --git a/packages/studio-ts/src/images/team/kathryn-murphy.jpg b/packages/studio-ts/src/images/team/kathryn-murphy.jpg new file mode 100644 index 0000000..8b6dede Binary files /dev/null and b/packages/studio-ts/src/images/team/kathryn-murphy.jpg differ diff --git a/packages/studio-ts/src/images/team/leonard-krasner.jpg b/packages/studio-ts/src/images/team/leonard-krasner.jpg new file mode 100644 index 0000000..57be1da Binary files /dev/null and b/packages/studio-ts/src/images/team/leonard-krasner.jpg differ diff --git a/packages/studio-ts/src/images/team/leslie-alexander.jpg b/packages/studio-ts/src/images/team/leslie-alexander.jpg new file mode 100644 index 0000000..6042215 Binary files /dev/null and b/packages/studio-ts/src/images/team/leslie-alexander.jpg differ diff --git a/packages/studio-ts/src/images/team/michael-foster.jpg b/packages/studio-ts/src/images/team/michael-foster.jpg new file mode 100644 index 0000000..a531ee6 Binary files /dev/null and b/packages/studio-ts/src/images/team/michael-foster.jpg differ diff --git a/packages/studio-ts/src/images/team/whitney-francis.jpg b/packages/studio-ts/src/images/team/whitney-francis.jpg new file mode 100644 index 0000000..2d92429 Binary files /dev/null and b/packages/studio-ts/src/images/team/whitney-francis.jpg differ diff --git a/packages/studio-ts/src/images/whiteboard.jpg b/packages/studio-ts/src/images/whiteboard.jpg new file mode 100644 index 0000000..e75bc5c Binary files /dev/null and b/packages/studio-ts/src/images/whiteboard.jpg differ diff --git a/packages/studio-ts/src/lib/formatDate.ts b/packages/studio-ts/src/lib/formatDate.ts new file mode 100644 index 0000000..013bd34 --- /dev/null +++ b/packages/studio-ts/src/lib/formatDate.ts @@ -0,0 +1,11 @@ +export function formatDate(dateString: string) { + let parts = dateString.split('-') + let hasDay = parts.length > 2 + + return new Date(`${dateString}Z`).toLocaleDateString('en-US', { + day: hasDay ? 'numeric' : undefined, + month: 'long', + year: 'numeric', + timeZone: 'UTC', + }) +} diff --git a/packages/studio-ts/src/lib/mdx.ts b/packages/studio-ts/src/lib/mdx.ts new file mode 100644 index 0000000..9ec6622 --- /dev/null +++ b/packages/studio-ts/src/lib/mdx.ts @@ -0,0 +1,65 @@ +import { type ImageProps } from 'next/image' +import glob from 'fast-glob' + +async function loadEntries( + directory: string, + metaName: string, +): Promise>> { + return ( + await Promise.all( + (await glob('**/page.mdx', { cwd: `src/app/${directory}` })).map( + async (filename) => { + let metadata = (await import(`../app/${directory}/${filename}`))[ + metaName + ] as T + return { + ...metadata, + metadata, + href: `/${directory}/${filename.replace(/\/page\.mdx$/, '')}`, + } + }, + ), + ) + ).sort((a, b) => b.date.localeCompare(a.date)) +} + +type ImagePropsWithOptionalAlt = Omit & { alt?: string } + +export type MDXEntry = T & { href: string; metadata: T } + +export interface Article { + date: string + title: string + description: string + author: { + name: string + role: string + image: ImagePropsWithOptionalAlt + } +} + +export interface CaseStudy { + date: string + client: string + title: string + description: string + summary: Array + logo: ImageProps['src'] + image: ImagePropsWithOptionalAlt + service: string + testimonial: { + author: { + name: string + role: string + } + content: string + } +} + +export function loadArticles() { + return loadEntries
    ('blog', 'article') +} + +export function loadCaseStudies() { + return loadEntries('work', 'caseStudy') +} diff --git a/packages/studio-ts/src/styles/base.css b/packages/studio-ts/src/styles/base.css new file mode 100644 index 0000000..ed1529a --- /dev/null +++ b/packages/studio-ts/src/styles/base.css @@ -0,0 +1,10 @@ +@tailwind base; + +@font-face { + font-family: 'Mona Sans'; + font-weight: 200 900; + font-display: block; + font-style: normal; + font-stretch: 75% 125%; + src: url('../fonts/Mona-Sans.var.woff2') format('woff2'); +} diff --git a/packages/studio-ts/src/styles/components.css b/packages/studio-ts/src/styles/components.css new file mode 100644 index 0000000..200eb2f --- /dev/null +++ b/packages/studio-ts/src/styles/components.css @@ -0,0 +1,3 @@ +@tailwind components; + +@import './typography.css'; diff --git a/packages/studio-ts/src/styles/tailwind.css b/packages/studio-ts/src/styles/tailwind.css new file mode 100644 index 0000000..4d7dae3 --- /dev/null +++ b/packages/studio-ts/src/styles/tailwind.css @@ -0,0 +1,3 @@ +@import './base.css'; +@import './components.css'; +@import './utilities.css'; diff --git a/packages/studio-ts/src/styles/typography.css b/packages/studio-ts/src/styles/typography.css new file mode 100644 index 0000000..3a63c73 --- /dev/null +++ b/packages/studio-ts/src/styles/typography.css @@ -0,0 +1,184 @@ +.typography { + color: theme(colors.neutral.950); + font-size: theme(fontSize.xl); + line-height: theme(fontSize.xl[1].lineHeight); + + --shiki-color-text: theme(colors.white); + --shiki-color-background: theme(colors.neutral.950); + --shiki-token-constant: theme(colors.neutral.300); + --shiki-token-string: theme(colors.neutral.400); + --shiki-token-comment: theme(colors.neutral.500); + --shiki-token-keyword: theme(colors.neutral.400); + --shiki-token-parameter: theme(colors.neutral.400); + --shiki-token-function: theme(colors.neutral.300); + --shiki-token-string-expression: theme(colors.neutral.300); + --shiki-token-punctuation: theme(colors.neutral.400); + + :where(.typography > *) { + margin-top: theme(spacing.6); + margin-bottom: theme(spacing.6); + } + + /* Headings */ + :where(h2) { + font-weight: theme(fontWeight.semibold); + font-family: theme(fontFamily.display); + font-variation-settings: theme(fontFamily.display[1].fontVariationSettings); + font-size: theme(fontSize.2xl); + line-height: theme(fontSize.2xl[1].lineHeight); + margin-top: theme(spacing.16); + } + + :where(h3) { + font-weight: theme(fontWeight.semibold); + font-family: theme(fontFamily.display); + font-variation-settings: theme(fontFamily.display[1].fontVariationSettings); + font-size: theme(fontSize.xl); + line-height: theme(fontSize.xl[1].lineHeight); + margin-top: theme(spacing.10); + } + + :where(h2 + h3) { + margin-top: 0; + } + + /* Lists */ + :where(ul, ol) { + padding-left: 1.5rem; + } + + :where(ul) { + list-style-type: disc; + } + + :where(ol) { + list-style-type: decimal; + } + + :where(li) { + padding-left: theme(spacing.3); + margin-top: theme(spacing.6); + } + + :where(li)::marker { + color: theme(colors.neutral.500); + } + + :where(li > *), + :where(li li) { + margin-top: theme(spacing.4); + } + + :where(ol > li)::marker { + font-size: theme(fontSize.base); + font-weight: theme(fontWeight.semibold); + } + + /* Tables */ + :where(table) { + width: 100%; + text-align: left; + font-size: theme(fontSize.base); + line-height: theme(fontSize.base[1].lineHeight); + } + + :where(th) { + font-weight: theme(fontWeight.semibold); + } + + :where(thead th) { + padding-bottom: theme(spacing.6); + border-bottom: 1px solid theme(colors.neutral.950); + } + + :where(td) { + vertical-align: top; + padding-top: theme(spacing.6); + padding-bottom: theme(spacing.6); + border-bottom: 1px solid theme(colors.neutral.950 / 0.1); + } + + :where(:is(th, td):not(:last-child)) { + padding-right: theme(spacing.6); + } + + /* Code blocks */ + :where(pre) { + display: flex; + background-color: theme(colors.neutral.950); + border-radius: theme(borderRadius.4xl); + overflow-x: auto; + margin-top: theme(spacing.10); + margin-bottom: theme(spacing.10); + margin-left: calc(-1 * theme(spacing.6)); + margin-right: calc(-1 * theme(spacing.6)); + + @screen sm { + margin-left: auto; + margin-right: auto; + } + } + + :where(pre code) { + flex: none; + padding: theme(padding.8) theme(padding.6); + font-size: theme(fontSize.base); + line-height: theme(lineHeight.8); + color: theme(colors.white); + + @screen sm { + padding: theme(spacing.10); + } + } + + /*
    */ + :where(hr) { + border-color: theme(colors.neutral.950 / 0.1); + margin-top: theme(spacing.24); + margin-bottom: theme(spacing.24); + } + + /* Inline text */ + :where(a) { + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.15em; + text-decoration-skip-ink: none; + font-weight: theme(fontWeight.semibold); + } + + :where(strong) { + font-weight: theme(fontWeight.semibold); + } + + :where(code:not(pre code)) { + font-size: calc(18 / 20 * 1em); + font-weight: theme(fontWeight.semibold); + + &::before, + &::after { + content: '`'; + } + } + + :where(h2 code, h3 code) { + font-weight: theme(fontWeight.bold); + } + + /* Figures */ + :where(figure) { + margin-top: theme(spacing.32); + margin-bottom: theme(spacing.32); + } + + /* Spacing overrides */ + :where(.typography:first-child > :first-child), + :where(li > :first-child) { + margin-top: 0 !important; + } + + :where(.typography:last-child > :last-child), + :where(li > :last-child) { + margin-bottom: 0 !important; + } +} diff --git a/packages/studio-ts/src/styles/utilities.css b/packages/studio-ts/src/styles/utilities.css new file mode 100644 index 0000000..65dd5f6 --- /dev/null +++ b/packages/studio-ts/src/styles/utilities.css @@ -0,0 +1 @@ +@tailwind utilities; diff --git a/packages/studio-ts/tailwind.config.ts b/packages/studio-ts/tailwind.config.ts new file mode 100644 index 0000000..03d9aa5 --- /dev/null +++ b/packages/studio-ts/tailwind.config.ts @@ -0,0 +1,34 @@ +import { type Config } from 'tailwindcss' +import defaultTheme from 'tailwindcss/defaultTheme' + +export default { + content: ['./src/**/*.{js,jsx,mdx,ts,tsx}'], + theme: { + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], + sm: ['0.875rem', { lineHeight: '1.5rem' }], + base: ['1rem', { lineHeight: '1.75rem' }], + lg: ['1.125rem', { lineHeight: '1.75rem' }], + xl: ['1.25rem', { lineHeight: '2rem' }], + '2xl': ['1.5rem', { lineHeight: '2.25rem' }], + '3xl': ['1.75rem', { lineHeight: '2.25rem' }], + '4xl': ['2rem', { lineHeight: '2.5rem' }], + '5xl': ['2.5rem', { lineHeight: '3rem' }], + '6xl': ['3rem', { lineHeight: '3.5rem' }], + '7xl': ['4rem', { lineHeight: '4.5rem' }], + }, + extend: { + borderRadius: { + '4xl': '2.5rem', + }, + fontFamily: { + sans: ['Mona Sans', ...defaultTheme.fontFamily.sans], + display: [ + ['Mona Sans', ...defaultTheme.fontFamily.sans], + { fontVariationSettings: '"wdth" 125' }, + ], + }, + }, + }, + plugins: [], +} satisfies Config diff --git a/packages/studio-ts/tsconfig.json b/packages/studio-ts/tsconfig.json new file mode 100644 index 0000000..13d6b45 --- /dev/null +++ b/packages/studio-ts/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/subnet/validator/main.py b/subnet/validator/main.py index 48e6493..7741279 100644 --- a/subnet/validator/main.py +++ b/subnet/validator/main.py @@ -1,10 +1,12 @@ import asyncio import os import bittensor as bt +from fastapi.params import Depends import torch from miner_manager import MinerManager from validator import BaseValidatorNeuron -from fastapi import FastAPI, Request +from fastapi import FastAPI, HTTPException, Request +from fastapi.security import OAuth2PasswordBearer, SecurityScopes import aiohttp from reward import calculate_total_message_length, get_reward from typing import TypedDict, List @@ -14,6 +16,9 @@ api_only = os.getenv('API_ONLY') +VALIDATOR_SECRET = os.getenv('VALIDATOR_SECRET') + + miner_manager = MinerManager(api_only=api_only == 'True') @@ -47,9 +52,14 @@ def __init__(self, config=None): async def index(): return "OK" +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="auth/token", scopes={"chat": "Access to chat endpoint"}) + @app.post("/chat") -async def chat(request: Request): +async def chat(request: Request, token: str = Depends(oauth2_scheme)): + if token != VALIDATOR_SECRET: + raise HTTPException(status_code=401, detail="Invalid token") data = await request.json() model = data['model'] miner = miner_manager.get_fastest_miner_for_model(model=model) diff --git a/subnet/validator/requirements.txt b/subnet/validator/requirements.txt index 23aa6c2..3cdf2a2 100644 --- a/subnet/validator/requirements.txt +++ b/subnet/validator/requirements.txt @@ -6,4 +6,5 @@ simplejson starlette uvicorn requests-async -loguru \ No newline at end of file +loguru +zkpy \ No newline at end of file diff --git a/subnet/validator/test.py b/subnet/validator/test.py new file mode 100644 index 0000000..cbfe9d0 --- /dev/null +++ b/subnet/validator/test.py @@ -0,0 +1,21 @@ +from zkpy.circuit import Circuit, GROTH, PLONK, FFLONK + +# Define the circuit that checks if a string contains another string +circuit = Circuit("./contains_string.circom") +circuit.compile() + +# Generate the witness (proof) for the statement "the string 'I love Python Programming' contains the string 'Python'" +circuit.gen_witness({"str": "I love Python Programming", "sub": "Python"}) + +# Set up the proving system using the PLONK scheme and the powers of tau file +circuit.setup(PLONK, ptau_file="ptau.ptau") + +# Generate the zero-knowledge proof +circuit.prove(PLONK) + +# Export the verification key and proof +circuit.export_vkey("vkey.json") +circuit.export_proof("proof.json", "public.json") + +# Verify the proof +circuit.verify(PLONK, "vkey.json", "public.json", "proof.json")