diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..db2c39586b --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,69 @@ +name: Deploy Documentation +on: + push: + branches: [ 'main' ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + timeout-minutes: 20 + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci skip')" + env: + PG_MAJOR: 14 + steps: + - uses: actions/checkout@v4 + - uses: bleep-build/bleep-setup-action@0.0.1 + - uses: coursier/cache-action@v6 + with: + extraFiles: bleep.yaml + + - name: Start up Postgres + run: docker compose up -d + + - name: Generate docs with mdoc + run: | + bleep generate-docs + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: site/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: ./site + + - name: Build website + run: npm run build + working-directory: ./site + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: ./site/build + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5063bdc4ea..2f4b4641f8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ target .vscode site/docs myproject/ +frontpage-generated/ diff --git a/CLAUDE.md b/CLAUDE.md index 9e16aac650..d60b290809 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -251,6 +251,45 @@ npm run serve - Check `bleep.yaml` for script configurations - Use database introspection tools to verify schema +### Working with Frontpage Code Examples + +The website's code examples are generated from real Typo code using the `frontpage` schema in the test database. This ensures that all examples shown on the website are accurate and compile correctly. + +**Schema Location**: `init/data/frontpage/` contains: +- `schema.sql` - Database schema for frontpage examples +- `*.sql` - SQL files that generate Typo repositories and types + +**Generation Process**: +1. **Modify Schema**: Edit files in `init/data/frontpage/` to change database structure or add new examples +2. **Restart Database**: Run `docker-compose down && docker-compose up -d` to apply schema changes +3. **Generate Code**: Run `bleep run GeneratedFrontpage` to generate fresh Scala code +4. **Update Website**: Manually copy the generated code from `frontpage-generated/` into website components in `site/src/components/FeatureShowcase/index.js` + +**Important Notes**: +- The `frontpage-generated/` directory is gitignored - we don't check in generated code +- The frontpage schema uses real PostgreSQL features (domains, enums, foreign keys) to showcase Typo's capabilities +- Always verify examples compile by running `bleep generate-docs` after updating website code +- This is the only part of the documentation workflow that requires manual copying of generated code +- Website logo and favicon can be regenerated using `site/scripts/generate-logo.js` and `npm run generate-favicon` + +**Example Workflow**: +```bash +# 1. Edit schema +vi init/data/frontpage/schema.sql + +# 2. Restart database +docker-compose down && docker-compose up -d + +# 3. Generate fresh code +bleep run GeneratedFrontpage + +# 4. Copy relevant parts to website (manual step) +# Look in frontpage-generated/ for the code you need + +# 5. Verify documentation builds +bleep generate-docs +``` + ## Troubleshooting ### Common Issues diff --git a/bleep.yaml b/bleep.yaml index ab47a467f2..445c1132f9 100644 --- a/bleep.yaml +++ b/bleep.yaml @@ -109,6 +109,9 @@ scripts: generate-docs: main: scripts.GenDocumentation project: typo-scripts-doc + generate-frontpage: + main: scripts.GeneratedFrontpage + project: typo-scripts generate-sources: main: scripts.GeneratedSources project: typo-scripts diff --git a/init/data/frontpage/complex-query.sql b/init/data/frontpage/complex-query.sql new file mode 100644 index 0000000000..9a607ff53e --- /dev/null +++ b/init/data/frontpage/complex-query.sql @@ -0,0 +1,9 @@ +-- Advanced parameter syntax example +SELECT p.*, a.city, e.salary +FROM frontpage.person p +JOIN frontpage.address a ON p.address_id = a.id +LEFT JOIN frontpage.employee e ON p.id = e.person_id +WHERE p.id = :person_id! + AND p.created_at >= :since! + AND a.country = :country:String? + AND (:max_salary? IS NULL OR e.salary <= :max_salary) \ No newline at end of file diff --git a/init/data/frontpage/schema.sql b/init/data/frontpage/schema.sql new file mode 100644 index 0000000000..6974948e59 --- /dev/null +++ b/init/data/frontpage/schema.sql @@ -0,0 +1,153 @@ +-- Schema for Typo front page examples +CREATE SCHEMA IF NOT EXISTS frontpage; + +-- PostgreSQL domains example (create first) +CREATE DOMAIN frontpage.email AS TEXT CHECK (VALUE ~ '^[^@]+@[^@]+\.[^@]+$'); + +-- Enum type example +CREATE TYPE frontpage.user_status AS ENUM ('active', 'inactive', 'suspended'); +CREATE TYPE frontpage.order_status AS ENUM ('pending', 'active', 'shipped', 'cancelled'); +CREATE TYPE frontpage.user_role AS ENUM ('admin', 'manager', 'employee'); + +-- Basic example tables +CREATE TABLE frontpage.department ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + budget DECIMAL(12,2) +); + +CREATE TABLE frontpage.user ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email frontpage.email NOT NULL UNIQUE, + name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + department_id UUID REFERENCES frontpage.department(id), + status frontpage.user_status DEFAULT 'active', + verified BOOLEAN DEFAULT false +); + +-- Relationships example tables +CREATE TABLE frontpage.product ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + price DECIMAL(10,2) NOT NULL, + in_stock BOOLEAN DEFAULT true, + quantity INTEGER DEFAULT 0, + last_restocked TIMESTAMP, + last_modified TIMESTAMP DEFAULT NOW(), + tags TEXT[] DEFAULT '{}', + categories INTEGER[] DEFAULT '{}', + prices DECIMAL[] DEFAULT '{}', + attributes JSONB[] DEFAULT '{}' +); + +CREATE TABLE frontpage.category ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL +); + +CREATE TABLE frontpage.product_category ( + product_id UUID REFERENCES frontpage.product(id), + category_id UUID REFERENCES frontpage.category(id), + PRIMARY KEY (product_id, category_id) +); + +CREATE TABLE frontpage.order ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES frontpage.user(id), + product_id UUID REFERENCES frontpage.product(id), + status frontpage.order_status DEFAULT 'pending', + total DECIMAL(10,2) NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + shipped_at TIMESTAMP +); + +CREATE TABLE frontpage.order_item ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID REFERENCES frontpage.order(id), + product_id UUID REFERENCES frontpage.product(id), + quantity INTEGER NOT NULL, + price DECIMAL(10,2) NOT NULL, + shipped_at TIMESTAMP +); + +-- Customers table for joins +CREATE TABLE frontpage.customer ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES frontpage.user(id), + company_name TEXT, + credit_limit DECIMAL(10,2), + verified BOOLEAN DEFAULT false +); + +-- Many-to-many example +CREATE TABLE frontpage.role ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE frontpage.user_role ( + user_id UUID REFERENCES frontpage.user(id), + role_id UUID REFERENCES frontpage.role(id), + assigned_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (user_id, role_id) +); + +-- Composite key example +CREATE TABLE frontpage.permission ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE frontpage.user_permission ( + user_id UUID REFERENCES frontpage.user(id), + permission_id UUID REFERENCES frontpage.permission(id), + granted_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (user_id, permission_id) +); + +-- Advanced PostgreSQL types example +CREATE TABLE frontpage.location ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + position POINT, + area POLYGON, + ip_range INET, + metadata JSONB DEFAULT '{}' +); + +-- Testing examples +CREATE TABLE frontpage.company ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL +); + +ALTER TABLE frontpage.department ADD COLUMN company_id UUID REFERENCES frontpage.company(id); +ALTER TABLE frontpage.user ADD COLUMN manager_id UUID REFERENCES frontpage.user(id); +ALTER TABLE frontpage.user ADD COLUMN role frontpage.user_role DEFAULT 'employee'; + +-- Complex join examples +CREATE TABLE frontpage.employee ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + person_id UUID UNIQUE NOT NULL, + salary DECIMAL(10,2) +); + +CREATE TABLE frontpage.person ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + address_id UUID, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE frontpage.address ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + city TEXT NOT NULL, + country TEXT NOT NULL +); + +ALTER TABLE frontpage.person ADD CONSTRAINT fk_address + FOREIGN KEY (address_id) REFERENCES frontpage.address(id); + +ALTER TABLE frontpage.employee ADD CONSTRAINT fk_person + FOREIGN KEY (person_id) REFERENCES frontpage.person(id); \ No newline at end of file diff --git a/init/data/frontpage/update-user-status.sql b/init/data/frontpage/update-user-status.sql new file mode 100644 index 0000000000..c832cdd966 --- /dev/null +++ b/init/data/frontpage/update-user-status.sql @@ -0,0 +1,12 @@ +-- sql/update-user-status.sql +UPDATE frontpage.user +SET + status = :new_status:frontpage.user_status!, + created_at = NOW() -- using created_at as modified_at since we don't have modified_at column +WHERE id = :user_id! + AND status != :new_status +RETURNING + id, + name, + status, + created_at as "modified_at:java.time.LocalDateTime!" \ No newline at end of file diff --git a/init/data/frontpage/user-analytics.sql b/init/data/frontpage/user-analytics.sql new file mode 100644 index 0000000000..03b76ef5e0 --- /dev/null +++ b/init/data/frontpage/user-analytics.sql @@ -0,0 +1,15 @@ +-- sql/user-analytics.sql +SELECT + u.name, + u.email, + COUNT(o.id) as order_count, + SUM(o.total) as lifetime_value, + MAX(o.created_at) as last_order_date +FROM frontpage.user u +LEFT JOIN frontpage.order o ON u.id = o.user_id +WHERE u.created_at >= :start_date:LocalDate! + AND u.status = :status:frontpage.user_status? +GROUP BY u.id, u.name, u.email +HAVING SUM(o.total) > :min_value:BigDecimal! +ORDER BY lifetime_value DESC +LIMIT :limit:Int! \ No newline at end of file diff --git a/init/install.sh b/init/install.sh index e87d36257c..76a3d6e986 100755 --- a/init/install.sh +++ b/init/install.sh @@ -12,3 +12,6 @@ psql -d Adventureworks < /docker-entrypoint-initdb.d/data/install.sql # this should have had a database by itself, but let's be lazy for now psql -d Adventureworks < /docker-entrypoint-initdb.d/data/test-tables.sql psql -d Adventureworks < /docker-entrypoint-initdb.d/data/issue148.sql + +# Front page examples +psql -d Adventureworks < /docker-entrypoint-initdb.d/data/frontpage/schema.sql diff --git a/site-in/customization/overview.md b/site-in/customization/overview.md index e9dcdf232b..8728acd521 100644 --- a/site-in/customization/overview.md +++ b/site-in/customization/overview.md @@ -39,6 +39,24 @@ val options = Options( | `rewriteDatabase` | Let's you perform arbitrary rewrites of database schema snapshot. you can add/remove rows, foreign keys and so on. | | `openEnums` | Controls if you want to tag tables ids as [open string enums](../type-safety/open-string-enums.md) | +## Database Libraries + +Typo supports multiple Scala database libraries, each with specific optimizations: + +- **Anorm** (`DbLibName.Anorm`) - Lightweight SQL parser for Play Framework +- **Doobie** (`DbLibName.Doobie`) - Functional JDBC layer for Cats Effect +- **ZIO-JDBC** (`DbLibName.ZioJdbc`) - Type-safe JDBC wrapper for ZIO + +Each library generates code optimized for that specific ecosystem, including appropriate return types, error handling, and integration patterns. + +```scala +val options = Options( + pkg = "myapp.db", + dbLib = Some(DbLibName.Anorm), // Choose your library + // ... other options +) +``` + ## Development options | Field Name | Effect | diff --git a/site-in/other-features/dsl-in-depth.md b/site-in/other-features/dsl-in-depth.md new file mode 100644 index 0000000000..e6a6ef4a85 --- /dev/null +++ b/site-in/other-features/dsl-in-depth.md @@ -0,0 +1,419 @@ +--- +title: DSL In-Depth Guide +--- + +# Typo DSL In-Depth Guide + +This guide provides comprehensive coverage of Typo's SQL DSL, including all its features and practical examples. + +```scala mdoc:invisible +import java.sql.{Connection, DriverManager} +import java.time.LocalDateTime +import adventureworks.customtypes.* +import adventureworks.production.product.* +import adventureworks.production.productmodel.* +import adventureworks.production.unitmeasure.* +import adventureworks.production.productcategory.* +import adventureworks.production.productsubcategory.* +import adventureworks.public.{Flag, Name} +import adventureworks.withConnection +import typo.dsl.* + +// Setup connection for all examples +implicit val c: Connection = DriverManager.getConnection("jdbc:postgresql://localhost:6432/Adventureworks?user=postgres&password=password") +c.setAutoCommit(false) + +// Repository instances for all examples +val productRepo = new ProductRepoImpl +val productmodelRepo = new ProductmodelRepoImpl +val unitmeasureRepo = new UnitmeasureRepoImpl +val productcategoryRepo = new ProductcategoryRepoImpl +val productsubcategoryRepo = new ProductsubcategoryRepoImpl + +// Mock data for examples - not actually inserted +val testUnitmeasure = UnitmeasureRow( + UnitmeasureId("test"), + Name("Test Unit"), + TypoLocalDateTime.now +) +val testProduct = ProductRow( + productid = ProductId(1), + name = Name("Test Product"), + productnumber = "TEST-001", + makeflag = Flag(true), + finishedgoodsflag = Flag(true), + color = Some("Red"), + safetystocklevel = TypoShort(10), + reorderpoint = TypoShort(5), + standardcost = BigDecimal(50), + listprice = BigDecimal(100), + size = Some("L"), + sizeunitmeasurecode = Some(UnitmeasureId("test")), + weightunitmeasurecode = Some(UnitmeasureId("test")), + weight = Some(1.5), + daystomanufacture = 7, + productline = Some("T "), + `class` = Some("H "), + style = Some("W "), + productsubcategoryid = Some(ProductsubcategoryId(1)), + productmodelid = Some(ProductmodelId(1)), + sellstartdate = TypoLocalDateTime.now, + sellenddate = None, + discontinueddate = None, + rowguid = TypoUUID.randomUUID, + modifieddate = TypoLocalDateTime.now +) +``` + +## Basic Selects + +The DSL provides a type-safe way to query your database. Every repository has a `select` method that returns a `SelectBuilder`: + +```scala mdoc:compile-only +// Simple select all +val allProducts: List[ProductRow] = productRepo.select.toList + +// Select specific rows +val product: Option[ProductRow] = productRepo.selectById(ProductId(1)) + +// Select by multiple IDs +val products: List[ProductRow] = productRepo.selectByIds(Array(ProductId(1), ProductId(2))) +``` + +## Where Clauses + +The `where` method allows you to filter results using type-safe predicates. + +**Note**: Consecutive calls to `where`, `orderBy`, and other query methods create an implicit **AND** operation. Multiple `where` clauses are combined with AND logic, and multiple `orderBy` clauses create a compound sort order. + +```scala mdoc:compile-only +// Simple equality +productRepo.select + .where(_.name === Name("Mountain Bike")) + .toList + +// Multiple conditions (implicitly ANDed) +productRepo.select + .where(_.color === Some("Red")) + .where(_.listprice > BigDecimal(1000)) + .where(_.daystomanufacture >= 5) + .toList + +// OR conditions +productRepo.select + .where(p => (p.color === Some("Red")).or(p.color === Some("Blue"))) + .toList + +// Complex predicates +productRepo.select + .where(p => p.listprice > BigDecimal(100) and p.listprice < BigDecimal(500)) + .where(_.name.like("Mountain%")) + .where(_.discontinueddate.isNull) + .toList + +// IN clause with arrays +productRepo.select + .where(p => p.productid.in(Array(ProductId(1), ProductId(22)))) + .toList + +// Complex boolean logic +productRepo.select + .where(x => (x.daystomanufacture > 25).or(x.daystomanufacture <= 0)) + .toList +``` + +### String Operations + +```scala mdoc:compile-only +// LIKE patterns +productRepo.select.where(_.name.like("Mountain%")) +productRepo.select.where(_.name.like("%Bike%")) + +// NOT LIKE +productRepo.select.where(p => !p.name.like("Road%")) + +// String length +productRepo.select.where(_.name.strLength > 10) + +// String concatenation +productRepo.select.where(p => (p.name.underlying || " - " || p.productnumber).like("%Special%")) +``` + +### Null Handling + +The DSL tracks nullability through the type system: + +```scala mdoc:compile-only +// Check for NULL +productRepo.select.where(_.color.isNull) +productRepo.select.where(p => !p.color.isNull) + +// COALESCE +productRepo.select.where(p => p.color.coalesce("Unknown") === "Red") + +// Working with non-nullable fields +productRepo.select.where(_.makeflag === Flag(true)) +``` + +**Note**: There's also a `whereStrict` variant that requires non-nullable predicates. This can be useful when you want to ensure at compile time that your predicate cannot be null, which helps when dealing with PostgreSQL's nullability semantics. Use regular `where` for most cases. + +## Order By + +Sort results using the `orderBy` method: + +```scala mdoc:compile-only +// Simple ordering +productRepo.select + .orderBy(_.name.asc) + .toList + +// Multiple sort criteria +productRepo.select + .orderBy(_.listprice.desc) + .orderBy(_.name.asc) + .toList + +// With null handling +productRepo.select + .orderBy(_.color.desc.withNullsFirst) + .orderBy(_.modifieddate.asc.withNullsFirst) + .toList +``` + +## Joins + +Typo supports various join types with type-safe predicates. + +### Inner Joins + +```scala mdoc:compile-only +// Simple inner join +val joinedData = productRepo.select + .join(unitmeasureRepo.select) + .on { case (product, unitmeasure) => + product.sizeunitmeasurecode === unitmeasure.unitmeasurecode + } + .toList + +// The result type is List[(ProductRow, UnitmeasureRow)] +``` + +### Left Joins + +Left joins return `Option[Row]` for the right side: + +```scala mdoc:compile-only +// Left join - note the Option type for the right side +val leftJoined: List[(ProductRow, Option[ProductmodelRow])] = + productRepo.select + .join(productmodelRepo.select) + .leftOn { case (product, productmodel) => + product.productmodelid === productmodel.productmodelid + } + .toList + +// Accessing fields from the optional side +leftJoined.foreach { case (product, maybeModel) => + println(s"Product: ${product.name}") + maybeModel.foreach(model => println(s" Model: ${model.name}")) +} +``` + +### Multiple Joins + +When performing multiple joins, the result builds up as nested tuples: + +```scala mdoc:compile-only +// Multiple joins create nested tuples: ((a, b), c) +val multiJoined = productRepo.select + .join(productmodelRepo.select) + .on { case (p, pm) => p.productmodelid === pm.productmodelid } + .join(productsubcategoryRepo.select) + .on { case ((p, _), ps) => p.productsubcategoryid === ps.productsubcategoryid } + .join(productcategoryRepo.select) + .on { case (((_, _), ps), pc) => ps.productcategoryid === pc.productcategoryid } + .toList + +// Type: List[(((ProductRow, ProductmodelRow), ProductsubcategoryRow), ProductcategoryRow)] +``` + +### Foreign Key Joins + +Typo provides a convenient `joinFk` method that leverages foreign key relationships: + +```scala mdoc:compile-only +// Using foreign key relationships +val fkJoined = productRepo.select + .joinFk(_.fkProductmodel)(productmodelRepo.select) + .joinFk(_._1.fkProductsubcategory)(productsubcategoryRepo.select) + .joinFk(_._2.fkProductcategory)(productcategoryRepo.select) + .toList + +// This is equivalent to the manual join above but more concise +``` + +### Handling Optionality in Left Joins + +When working with left joins, you can access fields from the optional side using a special syntax: + +```scala mdoc:compile-only +val leftJoinWithFilter = productRepo.select + .join(productmodelRepo.select) + .leftOn { case (p, pm) => p.productmodelid === pm.productmodelid } + .where { case (product, productModel) => + // Use the apply method to safely access optional fields + productModel(_.productmodelid) === product.productmodelid + } + .orderBy { case (_, productModel) => + // Optional ordering + productModel(_.name).desc.withNullsFirst + } + .toList +``` + +## Tuple Syntax with ~ + +Typo provides an alternative syntax for working with tuples using the `~` operator: + +```scala mdoc:compile-only +// Instead of nested tuples ((a, b), c), you can use a ~ b ~ c +val query = productRepo.select + .join(productmodelRepo.select) + .on { case (p, pm) => p.productmodelid === pm.productmodelid } + .join(productsubcategoryRepo.select) + .on { case (p ~ pm, ps) => p.productsubcategoryid === ps.productsubcategoryid } + .toList + +// Pattern matching with ~ +query.foreach { + case product ~ productModel ~ productSubcategory => + println(s"${product.name} - ${productModel.name} - ${productSubcategory.name}") +} + +// The ~ operator is defined as: +// type ~[A, B] = (A, B) +// with pattern matching support through unapply +``` + +## Printing SQL + +You can inspect the generated SQL for any query: + +```scala mdoc:compile-only +val query = productRepo.select + .where(_.listprice > BigDecimal(100)) + .join(productmodelRepo.select) + .on { case (p, pm) => p.productmodelid === pm.productmodelid } + .orderBy { case (p, _) => p.name.asc } + +// Print the SQL that will be executed +query.sql.foreach(println) + +// Example output: +// select "product_0"."productid", "product_0"."name", ... +// from "production"."product" "product_0" +// join "production"."productmodel" "productmodel_1" +// on "product_0"."productmodelid" = "productmodel_1"."productmodelid" +// where "product_0"."listprice" > ? +// order by "product_0"."name" asc +``` + +## Limit and Offset + +```scala mdoc:compile-only +// Get first 10 products +productRepo.select + .orderBy(_.name.asc) + .limit(10) + .toList + +// Pagination +productRepo.select + .orderBy(_.productid.asc) + .offset(20) + .limit(10) + .toList +``` + +## Update DSL + +The update DSL allows batch updates with type-safe predicates. Like other DSL operations, multiple calls to `setValue`, `setComputedValue`, and `where` methods are combined together - all set operations are applied to the same UPDATE statement, and multiple where clauses are ANDed together. + +```scala mdoc:compile-only +// Simple update +val updated = productRepo.update + .setValue(_.listprice)(BigDecimal(99.99)) + .setValue(_.modifieddate)(TypoLocalDateTime.now) + .where(_.productid === ProductId(1)) + .execute() + +// Update with computed values from the column +productRepo.update + .setComputedValue(_.listprice)(price => price * BigDecimal(1.1)) // 10% increase + .setComputedValue(_.reorderpoint)(_ + TypoShort(22)) + .where(_.productsubcategoryid === Some(ProductsubcategoryId(1))) + .execute() + +// Complex computed values with string operations +val update = productRepo.update + .setComputedValue(_.name)(p => (p.reverse.upper || Name("flaff")).substring(2, 4)) + .setValue(_.listprice)(BigDecimal(2)) + .setComputedValue(_.reorderpoint)(_ + TypoShort(22)) + .setComputedValue(_.sizeunitmeasurecode)(_ => Some(testUnitmeasure.unitmeasurecode)) + .where(_.productid === testProduct.productid) + +// Return updated rows +val updatedRows: List[ProductRow] = update.executeReturnChanged() + +// Print the SQL that will be executed (with RETURNING clause) +update.sql(returning = true).foreach(println) +``` + +## Delete DSL + +```scala mdoc:compile-only +// Delete with predicate +val deleted = productRepo.delete + .where(_.discontinueddate < TypoLocalDateTime.now) + .execute() + +// Delete by ID +productRepo.deleteById(ProductId(1)) + +// Delete multiple IDs +productRepo.deleteByIds(Array(ProductId(1), ProductId(2), ProductId(3))) +``` + +## Type Safety Features + +The DSL leverages Scala's type system to prevent common errors: + +1. **Column type safety**: You can't compare incompatible types +2. **Nullability tracking**: The DSL knows which columns are nullable +3. **Foreign key type safety**: ID types prevent joining on wrong columns +4. **Result type inference**: The compiler knows the exact shape of your results + +## Performance Considerations + +- The DSL generates efficient SQL with proper aliasing +- Joins are performed at the database level, not in memory +- Use `limit` for large result sets +- The generated SQL can be inspected using `.sql` + +## When to Use SQL Files + +While the DSL is powerful, it's designed for the "I just need to fetch/update this data" scenario. Use [SQL files](../what-is/sql-is-king.md) when you need: + +- Aggregations (GROUP BY, COUNT, SUM, etc.) +- Window functions +- CTEs (Common Table Expressions) +- Complex subqueries +- Database-specific features + +The DSL and SQL files complement each other - use the right tool for each job! + +```scala mdoc:invisible +// Clean up +c.rollback() +c.close() +``` \ No newline at end of file diff --git a/site-in/what-is/dsl.md b/site-in/what-is/dsl.md index 66b5ffac4d..f2eb4755d6 100644 --- a/site-in/what-is/dsl.md +++ b/site-in/what-is/dsl.md @@ -68,6 +68,7 @@ There is also a `delete` DSL, similar to `select` and `update`. It has no video ## Further reading +- [DSL In-Depth Guide](../other-features/dsl-in-depth.md) for comprehensive coverage of all DSL features with examples - [Getting started](../setup.md) for some information about how to set up the DSL. - [Limitations](../limitations.md) for a caveat on how PostgreSQL infers nullability. - [Customize sql files](../customization/customize-sql-files.md) for how to override parameter/column names, types and nullability diff --git a/site/docusaurus.config.js b/site/docusaurus.config.js index 93a31e8145..c4f06a1d6e 100644 --- a/site/docusaurus.config.js +++ b/site/docusaurus.config.js @@ -10,6 +10,18 @@ const config = { onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", favicon: "img/favicon.ico", + + // Modern browsers prefer SVG favicons + headTags: [ + { + tagName: 'link', + attributes: { + rel: 'icon', + type: 'image/svg+xml', + href: '/typo/img/favicon.svg', + }, + }, + ], // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. @@ -83,7 +95,7 @@ const config = { }, prism: { theme: require("prism-react-renderer").themes.github, - darkTheme: require("prism-react-renderer").themes.oceanicNext, + darkTheme: require("prism-react-renderer").themes.dracula, additionalLanguages: ["java", "scala", "yaml", "sql"], }, }), diff --git a/site/package.json b/site/package.json index 9f73e27068..8a26481136 100644 --- a/site/package.json +++ b/site/package.json @@ -12,7 +12,8 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "prettier": "prettier --write ." + "prettier": "prettier --write .", + "generate-favicon": "node scripts/generate-favicon.js && cd static/img && magick -density 300 -background transparent favicon.svg -resize 32x32 favicon.ico" }, "dependencies": { "@docusaurus/core": "^3.2.1", diff --git a/site/scripts/generate-favicon.js b/site/scripts/generate-favicon.js new file mode 100644 index 0000000000..9cfa16e9a6 --- /dev/null +++ b/site/scripts/generate-favicon.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); + +// Create a simpler version of the logo for favicon +const faviconSvgContent = ` + + + + + + + + + + + + + +`.trim(); + +// Write the SVG favicon +const faviconSvgPath = path.join(__dirname, '../static/img/favicon.svg'); +fs.writeFileSync(faviconSvgPath, faviconSvgContent); + +console.log('✅ Generated favicon.svg at:', faviconSvgPath); +console.log(''); +console.log('To generate the favicon.ico file, you can:'); +console.log('1. Use an online SVG to ICO converter (recommended for favicon.ico)'); +console.log('2. Use ImageMagick: convert -density 300 -background transparent favicon.svg -resize 32x32 favicon.ico'); +console.log('3. Use Inkscape: inkscape --export-png=temp.png --export-width=32 --export-height=32 favicon.svg && convert temp.png favicon.ico'); +console.log(''); +console.log('For modern browsers, you can also add this to your HTML head:'); +console.log(''); +console.log(''); +console.log('The generated favicon uses the same style as the logo but optimized for small sizes.'); \ No newline at end of file diff --git a/site/scripts/generate-logo.js b/site/scripts/generate-logo.js new file mode 100644 index 0000000000..83bef27c25 --- /dev/null +++ b/site/scripts/generate-logo.js @@ -0,0 +1,33 @@ +const fs = require('fs'); +const path = require('path'); + +// Create the SVG content as a string +const svgContent = ` + + + + + + + + + + + + + +`.trim(); + +// Write SVG file +const svgPath = path.join(__dirname, '../static/img/logo.svg'); +fs.writeFileSync(svgPath, svgContent); + +console.log('✅ Generated logo.svg at:', svgPath); +console.log('You can now use this SVG file or convert it to PNG using an online converter or tool like Inkscape.'); +console.log('For PNG conversion, you can:'); +console.log('1. Use an online SVG to PNG converter'); +console.log('2. Use Inkscape: inkscape --export-png=logo.png --export-width=512 --export-height=512 logo.svg'); +console.log('3. Use ImageMagick: convert -density 300 logo.svg -resize 512x512 logo.png'); \ No newline at end of file diff --git a/site/sidebars.js b/site/sidebars.js index c509b725d7..a8bdc97c18 100644 --- a/site/sidebars.js +++ b/site/sidebars.js @@ -47,10 +47,11 @@ const sidebars = { }, { type: "category", - label: "Other features", + label: "Advanced Features", collapsible: true, collapsed: false, items: [ + {type: "doc", id: "other-features/dsl-in-depth"}, {type: "doc", id: "other-features/streaming-inserts"}, {type: "doc", id: "other-features/generate-into-multiple-projects"}, {type: "doc", id: "other-features/json"}, diff --git a/site/src/components/DSLShowcase/index.js b/site/src/components/DSLShowcase/index.js new file mode 100644 index 0000000000..51a914e614 --- /dev/null +++ b/site/src/components/DSLShowcase/index.js @@ -0,0 +1,84 @@ +import React from "react"; +import CodeBlock from "@theme/CodeBlock"; +import styles from "./styles.module.css"; + +export default function DSLShowcase() { + return ( +
+
+
+
+

+ A DSL So Intuitive,
+ It Feels Like Cheating +

+

+ Write 80% of your queries without touching SQL. The DSL is type-safe, + composable, and works identically with real databases and in-memory stubs. +

+ +
+
+

✓ Full Type Safety

+

Every column, every operation, every join condition is checked at compile time.

+
+
+

✓ Intelligent Auto-complete

+

Your IDE knows exactly what columns are available at every step.

+
+
+

✓ Composable Queries

+

Build complex queries by combining simple, reusable parts.

+
+
+
+ +
+
+

Simple Yet Powerful

+ +{`// Find users with recent orders +val activeUsers = userRepo.select + .where(_.verified === true) + .where(_.createdAt > oneMonthAgo) + .orderBy(_.lastName.asc, _.firstName.asc) + .limit(100)`} + +
+ +
+

Type-Safe Joins

+ +{`// Join with compile-time safety +val userOrders = userRepo.select + .join(orderRepo.select) + .on(_.id, _.userId) // Types must match! + .where { case (user, order) => + user.country === "US" and + order.total > 100 + } + .map { case (user, order) => + UserOrderSummary(user.name, order.total) + }`} + +
+ +
+

Complex Conditions

+ +{`// Readable, composable predicates +productRepo.select.where { p => + (p.category === "Electronics" or + p.category === "Computers") and + p.price.between(100, 1000) and + p.inStock === true and + p.name.like("%Pro%") +}`} + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/site/src/components/DSLShowcase/styles.module.css b/site/src/components/DSLShowcase/styles.module.css new file mode 100644 index 0000000000..d856cb2572 --- /dev/null +++ b/site/src/components/DSLShowcase/styles.module.css @@ -0,0 +1,81 @@ +.dsl { + padding: 5rem 0; + background: linear-gradient(135deg, var(--typo-hero-bg-light) 0%, var(--ifm-background-color) 100%); +} + +[data-theme="dark"] .dsl { + background: linear-gradient(135deg, var(--typo-hero-bg-dark) 0%, var(--ifm-background-color) 100%); +} + +.content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; +} + +.title { + font-size: 2.5rem; + font-weight: 800; + line-height: 1.2; + margin-bottom: 1.5rem; +} + +.highlight { + background: var(--typo-gradient-accent); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.description { + font-size: 1.25rem; + line-height: 1.6; + color: var(--ifm-font-color-secondary); + margin-bottom: 2rem; +} + +.features { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.feature h4 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: var(--ifm-color-primary); +} + +.feature p { + margin: 0; + color: var(--ifm-font-color-secondary); +} + +.codeShowcase { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.codeExample h4 { + font-size: 1.1rem; + margin-bottom: 0.75rem; + color: var(--ifm-heading-color); +} + +.codeExample :global(.theme-code-block) { + margin: 0; + font-size: 0.85rem; +} + +@media (max-width: 1200px) { + .content { + grid-template-columns: 1fr; + gap: 3rem; + } + + .codeShowcase { + max-width: 800px; + } +} \ No newline at end of file diff --git a/site/src/components/FeatureShowcase/index.js b/site/src/components/FeatureShowcase/index.js new file mode 100644 index 0000000000..5830103070 --- /dev/null +++ b/site/src/components/FeatureShowcase/index.js @@ -0,0 +1,606 @@ +import React from "react"; +import Link from "@docusaurus/Link"; +import CodeBlock from "@theme/CodeBlock"; +import styles from "./styles.module.css"; + +const features = [ + { + category: "All The Boilerplate, None Of The Work", + items: [ + { + title: "From Database Schema to Complete Scala Code", + description: "Point Typo at your PostgreSQL database and watch it generate everything: case classes, repositories, type-safe IDs, JSON codecs, and test helpers. No manual mapping code ever again.", + sqlCode: `-- Your PostgreSQL schema +CREATE TABLE user ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + department_id UUID REFERENCES departments(id) +);`, + scalaCode: `// Generated automatically: +case class UserId(value: TypoUUID) +case class UserRow( + id: UserId, + email: String, + name: String, + createdAt: Option[TypoLocalDateTime], + departmentId: Option[DepartmentsId] +) + +trait UserRepo { + def selectAll(implicit c: Connection): List[UserRow] + def selectById(id: UserId)(implicit c: Connection): Option[UserRow] + def insert(unsaved: UserRowUnsaved)(implicit c: Connection): UserRow + def update(row: UserRow)(implicit c: Connection): Boolean + def deleteById(id: UserId)(implicit c: Connection): Boolean + // + 20 more methods +}`, + docs: "/docs/setup" + }, + { + title: "Complete CRUD + Advanced Operations", + description: "Get full repositories with not just basic CRUD, but batch operations, upserts, streaming inserts, and optional tracking methods. All generated, all type-safe.", + code: `// All generated automatically from your schema: + +// Basic operations +userRepo.selectById(UserId(uuid)) +userRepo.insert(unsavedUser) +userRepo.update(user.copy(name = "New Name")) +userRepo.deleteById(userId) + +// Batch operations +userRepo.upsertBatch(users) // Returns the upserted rows + +// Advanced operations +userRepo.selectByIds(userIds) +userRepo.selectByIdsTracked(userIds) // tracks found/missing +userRepo.insertStreaming(userStream) // PostgreSQL COPY API`, + docs: "/docs/what-is/relations" + } + ] + }, + { + category: "Relationships Become Navigation", + items: [ + { + title: "Foreign Keys Drive Everything", + description: "Every foreign key in your database automatically generates navigation methods, type-safe joins, and reverse lookups. Your schema relationships become first-class code citizens.", + sqlCode: `-- Database relationships +CREATE TABLE order ( + id UUID PRIMARY KEY, + user_id UUID REFERENCES user(id), + product_id UUID REFERENCES product(id) +);`, + scalaCode: `// Generated from foreign keys: +case class OrderRow( + id: OrderId, + userId: Option[UserId], // Type flows through relationships + productId: Option[ProductId] +) + +// Type-safe DSL with automatic foreign key joins: +orderRepo.select + .joinFk(_.fkUser)(userRepo.select) // Auto-joins via foreign key + .where { case (_, user) => user.email === Email("admin@company.com") } + +// joinFk knows the relationship from your schema! +// Your IDE will autocomplete available foreign keys`, + docs: "/docs/what-is/relations" + }, + { + title: "Type-Safe Foreign Key Navigation", + description: "Typo's DSL provides joinFk for easy type-safe navigation through foreign key relationships. Your IDE knows exactly what's available at each level.", + sqlCode: `-- Database with foreign key relationships +CREATE TABLE product ( + id UUID PRIMARY KEY, + model_id UUID REFERENCES product_model(id), + subcategory_id UUID REFERENCES product_subcategory(id) +); +CREATE TABLE product_subcategory ( + id UUID PRIMARY KEY, + category_id UUID REFERENCES product_category(id) +);`, + scalaCode: `// Navigate through multiple foreign keys with perfect type safety: +val query = productRepo.select + .joinFk(_.fkProductModel)(productModelRepo.select) + .joinFk { case (p, _) => p.fkProductSubcategory }(productSubcategoryRepo.select) + .joinFk { case ((_, _), ps) => ps.fkProductCategory }(productCategoryRepo.select) + .where { case (((product, model), subcategory), category) => + product.inStock === true && + category.name === "Electronics" + } + +// Each joinFk automatically uses the foreign key constraint +// No manual ON clauses needed - Typo knows the relationships!`, + docs: "/docs/other-features/dsl-in-depth" + } + ] + }, + { + category: "Type Safety Revolution", + items: [ + { + title: "Strongly-Typed Primary Keys", + description: "Every table gets its own ID type that flows through foreign key relationships. No more mixing up User IDs and Product IDs.", + code: `case class UserId(value: TypoUUID) +case class ProductId(value: TypoUUID) + +// Compile error if you mix them up! +def getUserOrders(userId: UserId): List[OrderRow] = { + orderRepo.select + .where(_.userId === userId.?) + .toList + // orderRepo.select.where(_.userId === productId.?) // ❌ Won't compile +}`, + docs: "/docs/type-safety/id-types" + }, + { + title: "Type Flow Through Relationships", + description: "Foreign key relationships automatically propagate specific types throughout your domain model.", + sqlCode: `-- Database schema creates type flow +CREATE TABLE user ( + id UUID PRIMARY KEY, + name TEXT +); + +CREATE TABLE order ( + id UUID PRIMARY KEY, + user_id UUID REFERENCES user(id) +);`, + scalaCode: `// Generated code maintains relationships +case class UserRow(id: UserId, name: String) +case class OrderRow(id: OrderId, userId: Option[UserId]) // ✅ Specific type, not just UUID`, + docs: "/docs/type-safety/type-flow" + }, + { + title: "PostgreSQL Domain Types", + description: "Full support for PostgreSQL domains with constraint documentation in your generated code.", + sqlCode: `-- Database domain +CREATE DOMAIN email AS TEXT CHECK (VALUE ~ '^[^@]+@[^@]+\\.[^@]+$');`, + scalaCode: `// Generated Scala code with constraint docs +/** Domain: frontpage.email + * Constraint: CHECK ((VALUE ~ '^[^@]+@[^@]+\\.[^@]+$'::text)) + */ +case class Email(value: String) + +// Usage in generated types: +case class UserRow(id: UserId, email: String) // Type preserved`, + docs: "/docs/type-safety/domains" + }, + { + title: "Composite Primary Keys", + description: "First-class support for composite primary keys with generated helper types and methods.", + sqlCode: `-- Composite key table +CREATE TABLE user_permission ( + user_id UUID REFERENCES user(id), + permission_id UUID REFERENCES permission(id), + granted_at TIMESTAMP, + PRIMARY KEY (user_id, permission_id) +);`, + scalaCode: `// Generated composite key row: +case class UserPermissionRow( + userId: UserId, + permissionId: PermissionId, + grantedAt: Option[TypoLocalDateTime] +) + +// Repository uses composite key directly: +userPermissionRepo.insert(UserPermissionRowUnsaved( + userId = userId, + permissionId = permissionId +))`, + docs: "/docs/type-safety/id-types#composite-keys" + } + ] + }, + { + category: "The Perfect DSL For Real-World Data Access", + items: [ + { + title: "Incredibly Easy To Work With", + description: "A pragmatic DSL that makes everyday data operations a breeze. Perfect IDE support with autocomplete, inline documentation, and compile-time validation. Focused on what you do most: fetching, updating, and deleting data with complex joins and filters.", + code: `// Fetch exactly the data you need with type-safe joins +val activeOrdersWithDetails = orderRepo.select + .join(customerRepo.select) + .on((o, c) => o.userId === c.userId) + .join(productRepo.select) + .on { case ((o, _), p) => o.productId === p.id.? } + .where { case ((order, _), _) => order.status === "active".? } + .where { case ((_, customer), _) => customer.verified === true.? } + .where { case (_, product) => product.inStock === true.? } + .orderBy { case ((order, _), _) => order.createdAt.desc } + .limit(100) + .toList // Execute and get results + +// Update with complex conditions +productRepo.update + .set(_.inStock, Some(false)) + .set(_.lastModified, Some(TypoLocalDateTime.now)) + .where(_.quantity === 0.?) + .where(_.lastRestocked < thirtyDaysAgo.?) + .execute + +// Delete with conditions +orderItemRepo.delete + .where(_.orderId.in(cancelledOrderIds)) + .where(_.shippedAt.isNull) + .execute`, + docs: "/docs/other-features/dsl-in-depth" + } + ] + }, + { + category: "Pure SQL Files as First-Class Citizens", + items: [ + { + title: "Write Real SQL For Complex Queries", + description: "When you need aggregations, window functions, or complex analytics, write real SQL in dedicated .sql files. Typo analyzes your queries and generates perfectly typed Scala methods - the best of both worlds.", + sqlCode: `-- sql/user-analytics.sql +SELECT + u.name, + u.email, + COUNT(o.id) as order_count, + SUM(o.total) as lifetime_value, + MAX(o.created_at) as last_order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.created_at >= :start_date:LocalDate! + AND u.status = :status:UserStatus? + AND (:min_orders? IS NULL OR COUNT(o.id) >= :min_orders) +GROUP BY u.id, u.name, u.email +HAVING SUM(o.total) > :min_value:BigDecimal! +ORDER BY lifetime_value DESC +LIMIT :limit:Int!`, + scalaCode: `// Generated automatically: +trait UserAnalyticsSqlRepo { + def apply( + startDate: LocalDate, + status: Option[String] = None, + minValue: BigDecimal, + limit: Int + )(implicit c: Connection): List[UserAnalyticsSqlRow] +}`, + docs: "/docs/what-is/sql-is-king" + }, + { + title: "Smart Parameter Inference", + description: "Typo analyzes your SQL parameters against the database schema to infer exact types. Override nullability and types as needed with simple annotations.", + code: `-- Advanced parameter syntax +SELECT p.*, a.city, e.salary +FROM persons p +JOIN addresses a ON p.address_id = a.id +LEFT JOIN employees e ON p.id = e.person_id +WHERE p.id = :person_id! -- Required parameter + AND p.created_at >= :since! -- Required parameter + AND a.country = :country:String? -- Optional string parameter + AND (:max_salary? IS NULL OR e.salary <= :max_salary) + +-- Dynamic filtering patterns work perfectly +-- Type inference follows foreign keys +-- Custom domain types are preserved`, + docs: "/docs/what-is/sql-is-king" + }, + { + title: "Updates with RETURNING Support", + description: "Write UPDATE, INSERT, and DELETE operations in SQL files. Full support for RETURNING clauses with type-safe result parsing.", + sqlCode: `-- sql/update-user-status.sql +UPDATE user +SET + status = :new_status:frontpage.user_status!, + created_at = NOW() +WHERE id = :user_id! + AND status != :new_status +RETURNING + id, + name, + status, + created_at as "modified_at:java.time.LocalDateTime!"`, + scalaCode: `// Generated method returns updated rows: +trait UpdateUserStatusSqlRepo { + def apply( + newStatus: String, + userId: TypoUUID + )(implicit c: Connection): List[UpdateUserStatusSqlRow] +} + +// Perfect for audit trails and optimistic locking`, + docs: "/docs/what-is/sql-is-king" + } + ] + }, + { + category: "Testing Excellence", + items: [ + { + title: "TestInsert: Build Valid Data Graphs", + description: "Generate complete object graphs with valid foreign key relationships. All fields are random by default, but you override exactly what your test cares about. Eliminates the 'lingering test state' problem forever.", + code: `val testInsert = new TestInsert(new Random(42)) + +// Build a complete, valid data graph +val company = testInsert.frontpageCompanies(name = "Acme Corp") +val department = testInsert.frontpageDepartments(companyId = Some(company.id)) +val manager = testInsert.frontpageUsers( + departmentId = Some(department.id), + role = Defaulted.Provided(Some(UserRole.manager)) +) +val employees = List.fill(5)( + testInsert.frontpageUsers( + departmentId = Some(department.id), + managerId = Some(manager.id), + role = Defaulted.Provided(Some(UserRole.employee)) + ) +) + +// Every foreign key is valid! +// All other fields are realistic random data! +// Zero lingering state between tests!`, + docs: "/docs/other-features/testing-with-random-values" + }, + { + title: "In-Memory Repository Stubs", + description: "Drop-in repository replacements that work entirely in memory. Run huge parts of your application without a database - perfect for unit tests and development.", + code: `// Replace real repos with in-memory stubs +val userRepo = UserRepoMock.empty +val orderRepo = OrdersRepoMock.empty +val productRepo = ProductsRepoMock.empty + +// Seed with test data +userRepo.insertUnsaved(testUsers: _*) +orderRepo.insertUnsaved(testOrders: _*) +productRepo.insertUnsaved(testProducts: _*) + +// Your entire business logic works! +val orderService = new OrderService(userRepo, orderRepo, productRepo) +val result = orderService.calculateMonthlyReport(userId) + +// Runs instantly, no database needed +// Full DSL support including complex joins`, + docs: "/docs/other-features/testing-with-stubs" + }, + { + title: "Full DSL Support in Stubs", + description: "Unlike other testing libraries, Typo's mocks support the complete DSL including complex joins and filtering. Your business logic runs unchanged.", + code: `// Complex queries work in memory! +val topCustomers = userRepo.select + .join(orderRepo.select) + .on((u, o) => u.id === o.userId.?) + .join(productRepo.select) + .on { case ((_, o), p) => o.productId === p.id.? } + .where { case ((user, _), _) => user.status === "active".? } + .where { case (_, product) => product.price > BigDecimal("100") } + .limit(50) + .toList + +// This runs instantly in memory! +// Same code as production database queries!`, + docs: "/docs/other-features/testing-with-stubs" + } + ] + }, + { + category: "Advanced PostgreSQL Integration", + items: [ + { + title: "Unprecedented PostgreSQL Array Support", + description: "First-class support for PostgreSQL arrays with type-safe operations. Use arrays naturally in queries with .in(), arrayOverlaps, arrayConcat, and array indexing.", + code: `// Full array support for all PostgreSQL types +case class ProductRow( + id: ProductsId, + name: String, + tags: Option[Array[String]], // TEXT[] + categories: Option[Array[Int]], // INTEGER[] + prices: Option[Array[BigDecimal]], // NUMERIC[] + attributes: Option[Array[TypoJsonb]] // JSONB[] +) + +// Array operations in queries +productRepo.select + .where(_.id.in(Array(id1, id2, id3))) + .where(_.tags.getOrElse(Array.empty).contains("sale")) + .toList`, + docs: "/docs/type-safety/arrays" + }, + { + title: "Other PostgreSQL Types & Features", + description: "Support for geometric types, network types, JSON/JSONB, XML, and more. If PostgreSQL has it, Typo supports it.", + code: `// Geometric and network types +case class LocationRow( + id: LocationsId, + position: Option[TypoPoint], // POINT + area: Option[TypoPolygon], // POLYGON + ipRange: Option[TypoInet], // INET + metadata: Option[TypoJsonb] // JSONB +) + +// Types are preserved and can be used in queries +locationRepo.select + .where(_.name === "Main Office") + .toList`, + docs: "/docs/type-safety/typo-types" + } + ] + }, + { + category: "Performance & Scalability", + items: [ + { + title: "Streaming Bulk Operations", + description: "PostgreSQL COPY API integration for high-performance bulk inserts and updates.", + code: `// Streaming insert using PostgreSQL COPY +val users = Iterator.range(1, 1000000).map(i => + UserRowUnsaved( + name = s"User \$i", + email = s"user\$i@example.com" + ) +) + +// Streams directly to PostgreSQL COPY API +val inserted = userRepo.insertUnsavedStreaming(users) +println(s"Inserted \$inserted records in seconds") + +// Batch operations - returns the upserted rows +val upsertedRows = userRepo.upsertBatch(usersList) +println(s"Upserted \${upsertedRows.length} rows")`, + docs: "/blog/the-cost-of-implicits" + }, + { + title: "Efficient Batch Operations", + description: "Optimized batch insert, update, and delete operations with detailed result tracking.", + code: `// True batch operations - single database roundtrip! +val newUsers = List( + UserRowUnsaved(email = Email("user1@example.com"), name = "User 1"), + UserRowUnsaved(email = Email("user2@example.com"), name = "User 2"), + UserRowUnsaved(email = Email("user3@example.com"), name = "User 3") +) + +// Batch upsert - returns all upserted rows +val upsertedUsers = userRepo.upsertBatch(newUsers) +println(s"Upserted \${upsertedUsers.length} users") + +// Batch delete by IDs +val deleted = userRepo.deleteByIds(Array(userId1, userId2, userId3)) +println(s"Deleted \$deleted rows") + +// Streaming batch operations for huge datasets +val millionUsers = Iterator.range(1, 1000000).map(i => + UserRowUnsaved(email = Email(s"user\$i@example.com"), name = s"User \$i") +) +userRepo.insertUnsavedStreaming(millionUsers) // Uses PostgreSQL COPY`, + docs: "/blog/the-cost-of-implicits" + } + ] + }, + { + category: "Multi-Library Support", + items: [ + { + title: "Choose Your Database Library", + description: "Full support for Anorm, Doobie, and ZIO-JDBC with library-specific optimizations.", + code: `// Anorm (Play Framework) +class UserController @Inject()(userRepo: UserRepo, db: Database) { + def getUser(id: UserId) = Action { + db.withConnection { implicit c => + userRepo.selectById(id) match { + case Some(user) => Ok(Json.toJson(user)) + case None => NotFound + } + } + } +} + +// Doobie (Cats Effect) +def getActiveUsers: ConnectionIO[List[UserRow]] = + userRepo.select + .where(user => user.status === "active".?) + .toList + +// ZIO-JDBC +def getUsersZIO: ZIO[Connection, Throwable, List[UserRow]] = + ZIO.serviceWithZIO[Connection](userRepo.selectAll(_))`, + docs: "/docs/customization/overview#database-libraries" + }, + { + title: "JSON Library Integration", + description: "Typo generates JSON codecs for Play JSON, Circe, and ZIO JSON - no manual derivation needed.", + code: `// Typo generates all JSON codecs for you! + +// Play JSON - generated in UserRow companion +implicit val usersReads: Reads[UserRow] = UserRow.reads +implicit val usersWrites: Writes[UserRow] = UserRow.writes + +// Circe - generated in UserRow companion +implicit val usersDecoder: Decoder[UserRow] = UserRow.decoder +implicit val usersEncoder: Encoder[UserRow] = UserRow.encoder + +// ZIO JSON - generated in UserRow companion +implicit val usersCodec: JsonCodec[UserRow] = UserRow.codec + +// Just use them - handles all complex types, arrays, nested objects +val json = Json.toJson(user) +val decoded = json.as[UserRow]`, + docs: "/docs/other-features/json" + } + ] + } +]; + +export default function FeatureShowcase() { + return ( +
+
+
+

+ Every Feature You Need, Nothing You Don't +

+

+ Typo delivers a comprehensive PostgreSQL development experience with unprecedented type safety, + testing capabilities, and developer productivity features. +

+
+ + {features.map((category, categoryIndex) => ( +
+

{category.category}

+
+ {category.items.map((feature, featureIndex) => ( +
+
+

{feature.title}

+

{feature.description}

+
+ {feature.sqlCode && ( + + {feature.sqlCode} + + )} + {feature.scalaCode && ( + + {feature.scalaCode} + + )} + {feature.code && ( + + {feature.code} + + )} +
+ + Learn More → + +
+
+ ))} +
+
+ ))} + +
+

And Much More...

+
+
+ Advanced Customization: Type overrides, nullability control, custom naming conventions +
+
+ Enterprise Ready: Transaction support, CI/CD integration, version control friendly +
+
+ Developer Experience: Real-time code generation, IDE integration, comprehensive logging +
+
+ PostgreSQL Deep Integration: Comprehensive array support with operations, enums, domains, geometric types, network types +
+
+
+ + Explore All Features + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/site/src/components/FeatureShowcase/styles.module.css b/site/src/components/FeatureShowcase/styles.module.css new file mode 100644 index 0000000000..19f3850b2e --- /dev/null +++ b/site/src/components/FeatureShowcase/styles.module.css @@ -0,0 +1,377 @@ +.featureShowcase { + padding: 6rem 0; + background: var(--ifm-background-surface-color); + position: relative; + overflow: hidden; +} + +.featureShowcase::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 300px; + background: radial-gradient(ellipse at top, rgba(139, 92, 246, 0.03) 0%, transparent 50%); + pointer-events: none; +} + +.header { + text-align: center; + margin-bottom: 4rem; +} + +.title { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 1.5rem; + background: var(--typo-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + position: relative; + animation: fadeInUp 0.8s ease-out; +} + +.title::after { + content: ''; + position: absolute; + bottom: -12px; + left: 50%; + transform: translateX(-50%); + width: 120px; + height: 3px; + background: var(--typo-gradient-primary); + border-radius: 2px; + animation: slideInFromCenter 0.8s ease-out 0.3s both; +} + +@keyframes slideInFromCenter { + from { + width: 0; + opacity: 0; + } + to { + width: 120px; + opacity: 1; + } +} + +.subtitle { + font-size: 1.25rem; + line-height: 1.6; + color: var(--ifm-font-color-secondary); + max-width: 800px; + margin: 0 auto; + animation: fadeInUp 0.8s ease-out 0.2s both; +} + +.categorySection { + margin-bottom: 6rem; + position: relative; +} + +.categorySection::before { + content: ''; + position: absolute; + top: -1rem; + left: 50%; + transform: translateX(-50%); + width: 100px; + height: 1px; + background: var(--ifm-color-emphasis-300); +} + +.categoryTitle { + font-size: 2.25rem; + font-weight: 700; + margin-bottom: 3rem; + background: var(--typo-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-align: center; + position: relative; + padding: 1rem 0; + animation: fadeInUp 0.8s ease-out; +} + +.categoryTitle::after { + content: ''; + position: absolute; + bottom: -8px; + left: 50%; + transform: translateX(-50%); + width: 80px; + height: 4px; + background: var(--typo-gradient-primary); + border-radius: 2px; + animation: slideInFromLeft 0.8s ease-out 0.3s both; +} + +@keyframes slideInFromLeft { + from { + width: 0; + opacity: 0; + } + to { + width: 80px; + opacity: 1; + } +} + +.featuresGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 3rem; + margin-bottom: 4rem; +} + +.featureCard { + background: var(--ifm-card-background-color); + border-radius: 16px; + padding: 2.5rem; + box-shadow: 0 6px 16px rgba(139, 92, 246, 0.08); + border: 1px solid var(--ifm-color-emphasis-200); + transition: all var(--typo-transition-normal); + position: relative; + overflow: hidden; +} + +.featureCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--typo-gradient-primary); + transform: translateY(-100%); + transition: transform var(--typo-transition-normal); +} + + +.featureCard:hover::before { + transform: translateY(0); +} + + +.featureCard:hover { + transform: translateY(-8px) scale(1.01); + box-shadow: 0 20px 40px rgba(139, 92, 246, 0.2); + border-color: var(--ifm-color-primary); +} + +.featureCard::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 30% 20%, rgba(139, 92, 246, 0.03) 0%, transparent 50%); + opacity: 0; + transition: opacity var(--typo-transition-normal); + pointer-events: none; +} + +.featureCard:hover::after { + opacity: 1; +} + +.featureHeader { + margin-bottom: 1.25rem; + position: relative; +} + +.featureTitle { + font-size: 1.4rem; + font-weight: 700; + margin-bottom: 0.75rem; + color: var(--ifm-heading-color); + line-height: 1.3; +} + +.featureDescription { + color: var(--ifm-font-color-secondary); + line-height: 1.6; + margin: 0; + font-size: 1rem; +} + +.featureCode { + margin: 1.25rem 0; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); + max-width: 100%; + position: relative; + border: 1px solid var(--ifm-color-emphasis-200); + transition: all var(--typo-transition-normal); +} + +.featureCode:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); +} + +.featureCode::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: var(--typo-gradient-primary); + z-index: 1; +} + +.featureCode:last-of-type { + margin-bottom: 2rem; +} + +.featureCode :global(.theme-code-block) { + margin: 0; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + border-radius: 0; +} + +.featureCode :global(.theme-code-block pre) { + padding: 1rem; + font-size: 0.85rem; + line-height: 1.4; + max-height: 300px; + overflow-y: auto; + margin: 0; + border-radius: 0; +} + +.featureCode :global(.theme-code-block pre code) { + font-family: var(--ifm-font-family-monospace); + font-size: 0.85rem; + background: transparent; + padding: 0; +} + +.featureCode :global(.codeBlockContent_node_modules-\\@docusaurus-theme-classic-lib-theme-CodeBlock-Content-styles-module) { + font-family: var(--ifm-font-family-monospace); +} + +.featureFooter { + display: flex; + justify-content: flex-end; +} + +.featureLink { + color: var(--ifm-color-primary); + text-decoration: none; + font-weight: 600; + font-size: 0.9rem; + transition: color var(--typo-transition-fast); +} + +.featureLink:hover { + color: var(--ifm-color-primary-dark); + text-decoration: none; +} + +.moreFeatures { + background: var(--ifm-background-color); + border-radius: 16px; + padding: 3rem; + text-align: center; + border: 1px solid var(--ifm-color-emphasis-200); + margin-top: 3rem; +} + +.moreFeaturesTitle { + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 2rem; + color: var(--ifm-heading-color); +} + +.moreFeaturesList { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2.5rem; +} + +.moreFeatureItem { + background: var(--ifm-card-background-color); + padding: 1.5rem; + border-radius: 8px; + border: 1px solid var(--ifm-color-emphasis-200); + text-align: left; + line-height: 1.6; + transition: all var(--typo-transition-normal); +} + +.moreFeatureItem:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.1); +} + +.moreFeatureItem strong { + color: var(--ifm-color-primary); + font-weight: 600; +} + +.moreFeaturesCTA { + margin-top: 2rem; +} + +/* Dark mode adjustments */ +[data-theme="dark"] .featureCard { + background: var(--typo-gray-200); + border-color: var(--typo-gray-300); +} + +[data-theme="dark"] .moreFeatures { + background: var(--typo-gray-200); + border-color: var(--typo-gray-300); +} + +[data-theme="dark"] .moreFeatureItem { + background: var(--typo-gray-100); + border-color: var(--typo-gray-300); +} + +/* Responsive design */ +@media (max-width: 1200px) { + .featuresGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .featureShowcase { + padding: 4rem 0; + } + + .title { + font-size: 2rem; + } + + .categoryTitle { + font-size: 1.5rem; + } + + .featureCard { + padding: 1.5rem; + } + + .moreFeatures { + padding: 2rem; + } + + .moreFeaturesList { + grid-template-columns: 1fr; + gap: 1rem; + } + + .moreFeatureItem { + padding: 1.25rem; + } +} \ No newline at end of file diff --git a/site/src/components/HomepageFeatures/index.js b/site/src/components/HomepageFeatures/index.js index 751c00b4c9..3846aae878 100644 --- a/site/src/components/HomepageFeatures/index.js +++ b/site/src/components/HomepageFeatures/index.js @@ -1,47 +1,108 @@ import React from "react"; import clsx from "clsx"; +import CodeBlock from "@theme/CodeBlock"; import styles from "./styles.module.css"; const FeatureList = [ { - title: "Effortless integration with PostgreSQL", - Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default, + title: "SQL First", + icon: "📝", description: ( <> - Write SQL as SQL! + Write SQL in .sql files with full IDE support. No ORMs or query builders required - just pure SQL with type-safe parameters. ), + codeExample: `-- users.sql +SELECT * FROM users +WHERE email = :email!`, + codeLanguage: "sql", }, { - title: "Type-safety", - Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default, + title: "Type-Safe Everything", + icon: "🔒", description: ( <> - Typo's brings contract-driven development to the database layer + Complete type safety from database to application. Foreign keys become specific ID types, nullable columns become Option[T]. ), + codeExample: `case class UserId(value: Long) +case class User( + id: UserId, + email: String, + name: Option[String] +)`, }, { - title: "Effortless CRUD", - Svg: require("@site/static/img/undraw_docusaurus_react.svg").default, + title: "Zero Boilerplate", + icon: "⚡", description: ( <> - Extend or customize your website layout by reusing React. Docusaurus can - be extended while reusing the same header and footer. + Generates repositories with CRUD operations, streaming queries, and batch inserts. Works with Anorm, Doobie, and ZIO-JDBC. ), + codeExample: `UserRepo.insert(user) +UserRepo.selectById(userId) +UserRepo.updateEmail(userId, email) +UserRepo.selectAll.stream`, + }, + { + title: "Functional Relational Mapping", + icon: "🚀", + description: ( + <> + Not an ORM - it's FRM. Maps your database schema to immutable case classes without runtime overhead or magic. Fast compilation, zero reflection. + + ), + codeExample: `// FRM: Pure functions over data +// vs ORM: Complex object hierarchies +// vs hand-written SQL: Verbose boilerplate +// vs jOOQ: Better testing story`, + codeLanguage: "scala", + }, + { + title: "Stream Like a Pro", + icon: "🌊", + description: ( + <> + Built-in streaming support for large datasets. Process millions of rows without breaking a sweat using your favorite streaming library. + + ), + codeExample: `// Stream millions of rows efficiently +UserRepo.selectAll.stream + .filter(_.active) + .mapAsync(enrichUser) + .runWith(Sink.foreach(process))`, + }, + { + title: "Powerful Query DSL", + icon: "🎯", + description: ( + <> + Optional type-safe DSL for complex queries. Build dynamic queries with compile-time guarantees and autocomplete support. + + ), + codeExample: `select + .from(users) + .join(posts).on(_.id, _.userId) + .where(_.email.like("%@typo%")) + .orderBy(_.createdAt.desc) + .limit(10)`, }, ]; -function Feature({Svg, title, description}) { +function Feature({icon, title, description, codeExample, codeLanguage = "scala", index}) { return ( -
- {/*
*/} - {/* */} - {/*
*/} -
-

{title}

-

{description}

+
+
+

{title}

+
{description}
+ {codeExample && ( +
+ + {codeExample} + +
+ )}
); @@ -51,9 +112,10 @@ export default function HomepageFeatures() { return (
-
+

Why Developers Love Typo

+
{FeatureList.map((props, idx) => ( - + ))}
diff --git a/site/src/components/HomepageFeatures/styles.module.css b/site/src/components/HomepageFeatures/styles.module.css index b248eb2e5d..61031d36b0 100644 --- a/site/src/components/HomepageFeatures/styles.module.css +++ b/site/src/components/HomepageFeatures/styles.module.css @@ -1,11 +1,217 @@ .features { display: flex; align-items: center; - padding: 2rem 0; + padding: 5rem 0; width: 100%; + background: var(--ifm-background-surface-color); } -.featureSvg { - height: 200px; - width: 200px; +.featuresTitle { + text-align: center; + font-size: 2.5rem; + margin-bottom: 1rem; + color: var(--ifm-heading-color); + animation: fadeIn 0.8s ease-out; } + +.featuresSubtitle { + text-align: center; + font-size: 1.25rem; + color: var(--ifm-font-color-secondary); + margin-bottom: 3rem; + max-width: 700px; + margin-left: auto; + margin-right: auto; + line-height: 1.6; +} + +.featureGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; + margin: 0 auto; +} + +.featureCard { + padding: 1.5rem; + border-radius: 12px; + background: var(--ifm-card-background-color); + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.06); + transition: all var(--typo-transition-normal); + animation: fadeInUp 0.8s ease-out both; + animation-delay: calc(var(--index) * 0.1s); + position: relative; + overflow: hidden; + text-align: center; + border: 1px solid var(--ifm-color-emphasis-200); + height: 280px; + backdrop-filter: blur(10px); + cursor: pointer; +} + +.featureCard > div { + position: relative; + top: 50%; + transform: translateY(-50%); +} + +.featureCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--typo-gradient-primary); + transform: translateX(-100%); + transition: transform var(--typo-transition-normal); +} + +.featureCard:hover::before { + transform: translateX(0); +} + +.featureCard:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 20px 40px rgba(139, 92, 246, 0.15); + border-color: var(--ifm-color-primary); +} + +.featureCard::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 50% 0%, rgba(139, 92, 246, 0.05) 0%, transparent 50%); + opacity: 0; + transition: opacity var(--typo-transition-normal); +} + +.featureCard:hover::after { + opacity: 1; +} + +.featureIcon { + font-size: 2.5rem; + margin-bottom: 0.75rem; + display: block; + line-height: 1; + transition: all var(--typo-transition-normal); + position: relative; + z-index: 2; + filter: drop-shadow(0 2px 4px rgba(139, 92, 246, 0.2)); +} + +.featureCard:hover .featureIcon { + transform: scale(1.2) rotate(5deg); + filter: drop-shadow(0 4px 8px rgba(139, 92, 246, 0.3)); +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.featureTitle { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--ifm-heading-color); + font-weight: 700; + line-height: 1.2; +} + +.featureDescription { + margin-bottom: 1rem; + color: var(--ifm-font-color-secondary); + line-height: 1.5; + font-size: 0.9rem; +} + +.featureDescription code { + background: var(--ifm-code-background); + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-size: 0.9em; + color: var(--typo-accent); +} + +.codeExample { + margin-top: 0.75rem; + border-radius: 8px; + overflow: hidden; + opacity: 0; + animation: fadeIn 0.5s ease-out 0.5s forwards; + text-align: left; +} + +/* Override CodeBlock default styles for compact display */ +.codeExample :global(.theme-code-block) { + margin: 0; + box-shadow: none; +} + +.codeExample :global(.theme-code-block pre) { + margin: 0; + padding: 0.4rem 0.6rem; + font-size: 0.7rem; + line-height: 1.3; + max-height: 80px; + overflow-y: auto; +} + +.codeExample :global(.codeBlockContent_node_modules-\@docusaurus-theme-classic-lib-theme-CodeBlock-Content-styles-module) { + font-size: 0.75rem; +} + +/* Hide copy button for cleaner look in small cards */ +.codeExample :global(.buttonGroup__atx) { + display: none; +} + +/* Responsive adjustments */ +@media (max-width: 996px) { + .featureCard { + margin-bottom: 2rem; + } + + .featuresTitle { + font-size: 2rem; + } +} + +/* Dark mode adjustments */ +[data-theme='dark'] .featureCard { + background: var(--typo-gray-200); + border: 1px solid var(--typo-gray-300); +} + +[data-theme='dark'] .featureDescription code { + background: var(--typo-gray-300); + color: var(--typo-accent-light); +} + +[data-theme='dark'] .codeExample { + background: var(--typo-gray-100); + border-color: var(--typo-gray-300); +} + +/* Grid responsiveness */ +@media (max-width: 1200px) { + .featureGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .featureGrid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/site/src/components/MagicalWizard/index.js b/site/src/components/MagicalWizard/index.js new file mode 100644 index 0000000000..7e1db2ba69 --- /dev/null +++ b/site/src/components/MagicalWizard/index.js @@ -0,0 +1,56 @@ +import React from 'react'; +import styles from './styles.module.css'; + +const MagicalWizard = ({ size = 'medium', spell = 'Type Safety' }) => { + const sizeClass = { + small: styles.wizardSmall, + medium: styles.wizardMedium, + large: styles.wizardLarge + }; + + return ( +
+
+ {/* Wizard Hat */} +
+
+
+ + {/* Wizard Head */} +
+
+
+
+
+
+
+ + {/* Wizard Body */} +
+ + {/* Wizard Beard */} +
+ + {/* Magic Staff */} +
+
+
+
+ + {/* Magic Sparkles */} +
+
+
💫
+
+
+ + {/* Speech Bubble */} +
+
{spell}
+
+
+
+ ); +}; + +export default MagicalWizard; \ No newline at end of file diff --git a/site/src/components/MagicalWizard/styles.module.css b/site/src/components/MagicalWizard/styles.module.css new file mode 100644 index 0000000000..537f40388c --- /dev/null +++ b/site/src/components/MagicalWizard/styles.module.css @@ -0,0 +1,277 @@ +/* 🧙‍♂️ MAGICAL WIZARD COMPONENT */ +.wizard { + display: inline-block; + position: relative; + margin: 1rem; +} + +.wizardContainer { + position: relative; + animation: wizardFloat 3s ease-in-out infinite; +} + +.wizardSmall { + transform: scale(0.6); +} + +.wizardMedium { + transform: scale(1); +} + +.wizardLarge { + transform: scale(1.5); +} + +/* Wizard Hat */ +.wizardHat { + width: 60px; + height: 40px; + background: linear-gradient(135deg, #4f46e5, #7c3aed); + border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; + position: relative; + margin: 0 auto; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3); +} + +.hatStar { + position: absolute; + top: -5px; + left: 50%; + transform: translateX(-50%); + font-size: 20px; + animation: starTwinkle 2s ease-in-out infinite; +} + +/* Wizard Head */ +.wizardHead { + width: 50px; + height: 50px; + background: #fbbf24; + border-radius: 50%; + position: relative; + margin: -10px auto 0; + box-shadow: 0 2px 10px rgba(251, 191, 36, 0.3); +} + +.wizardEyes { + display: flex; + justify-content: space-between; + padding: 15px 12px 0; +} + +.eye { + width: 8px; + height: 8px; + background: #334155; + border-radius: 50%; + position: relative; +} + +.eye::after { + content: ''; + width: 3px; + height: 3px; + background: white; + border-radius: 50%; + position: absolute; + top: 1px; + left: 2px; +} + +.wizardSmile { + width: 20px; + height: 10px; + border: 2px solid #334155; + border-top: none; + border-radius: 0 0 20px 20px; + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); +} + +/* Wizard Body */ +.wizardBody { + width: 60px; + height: 80px; + background: linear-gradient(135deg, #6366f1, #8b5cf6, #ec4899); + border-radius: 30px 30px 50px 50px; + margin: 0 auto; + box-shadow: 0 4px 20px rgba(139, 92, 246, 0.2); +} + +/* Wizard Beard */ +.wizardBeard { + width: 30px; + height: 20px; + background: #e2e8f0; + border-radius: 0 0 15px 15px; + position: absolute; + bottom: 70px; + left: 50%; + transform: translateX(-50%); + opacity: 0.8; +} + +/* Magic Staff */ +.magicStaff { + position: absolute; + left: -20px; + top: 30px; +} + +.staffHandle { + width: 4px; + height: 60px; + background: linear-gradient(to bottom, #fbbf24, #f59e0b); + border-radius: 2px; + position: relative; +} + +.staffCrystal { + width: 12px; + height: 18px; + background: linear-gradient(135deg, #06b6d4, #0891b2); + border-radius: 50%; + position: absolute; + top: -8px; + left: -4px; + box-shadow: 0 0 15px rgba(6, 182, 212, 0.5); + animation: crystalGlow 2s ease-in-out infinite; +} + +/* Magic Sparkles */ +.sparkles { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.sparkle { + position: absolute; + font-size: 12px; + animation: sparkleFloat 3s ease-in-out infinite; +} + +.sparkle1 { + top: 10px; + right: 10px; + animation-delay: 0s; +} + +.sparkle2 { + top: 40px; + right: -10px; + animation-delay: 1s; +} + +.sparkle3 { + top: 70px; + right: 5px; + animation-delay: 2s; +} + +/* Speech Bubble */ +.speechBubble { + position: absolute; + top: -40px; + right: -80px; + background: rgba(255, 255, 255, 0.95); + padding: 8px 12px; + border-radius: 15px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + opacity: 0; + transform: translateY(10px); + animation: speechAppear 4s ease-in-out infinite; +} + +.speechBubble::before { + content: ''; + position: absolute; + bottom: -8px; + left: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid rgba(255, 255, 255, 0.95); +} + +.speechText { + font-size: 10px; + font-weight: 600; + color: #6366f1; + white-space: nowrap; +} + +/* Animations */ +@keyframes wizardFloat { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-8px); + } +} + +@keyframes starTwinkle { + 0%, 100% { + transform: translateX(-50%) scale(1); + opacity: 1; + } + 50% { + transform: translateX(-50%) scale(1.2); + opacity: 0.7; + } +} + +@keyframes crystalGlow { + 0%, 100% { + box-shadow: 0 0 15px rgba(6, 182, 212, 0.5); + } + 50% { + box-shadow: 0 0 25px rgba(6, 182, 212, 0.8); + } +} + +@keyframes sparkleFloat { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + opacity: 0.6; + } + 50% { + transform: translateY(-5px) rotate(180deg); + opacity: 1; + } +} + +@keyframes speechAppear { + 0%, 70% { + opacity: 0; + transform: translateY(10px); + } + 80%, 90% { + opacity: 1; + transform: translateY(0px); + } + 100% { + opacity: 0; + transform: translateY(-5px); + } +} + +/* Hover Effects */ +.wizard:hover .wizardContainer { + animation-duration: 1s; +} + +.wizard:hover .speechBubble { + animation-duration: 2s; +} + +.wizard:hover .sparkle { + animation-duration: 1.5s; +} \ No newline at end of file diff --git a/site/src/components/ScalaCompileTimesChart.jsx b/site/src/components/ScalaCompileTimesChart.jsx index 3ee69815ed..d48e16baa6 100644 --- a/site/src/components/ScalaCompileTimesChart.jsx +++ b/site/src/components/ScalaCompileTimesChart.jsx @@ -29,80 +29,79 @@ const ScalaCompileTimesChart = ({id, children: csvData}) => { return `hsl(${baseHue}, 70%, ${lightness}%)`; }; - const updateChart = view => { - if (chartInstance) { - chartInstance.destroy(); - } + // Clean up any existing chart before creating a new one + let existingChart = Chart.getChart(id); + if (existingChart) { + existingChart.destroy(); + } - const chartData = { - labels: libraries, - datasets: scalaVersions.flatMap(scalaVersion => { - return inlinedImplicits.map(inlined => { - const data = libraries.map(library => { - const rowData = rows.find( - row => - row[0] === library && - row[1] === scalaVersion && - row[2] === inlined - ); - return rowData - ? view === 'avg' - ? parseFloat(rowData[3]) - : parseFloat(rowData[4]) - : 0; - }); - return { - label: (inlinedImplicits.length === 1) ? scalaVersion : `${scalaVersion} - Inlined: ${inlined}`, - backgroundColor: generateColor(scalaVersion, inlined), - data: data, - }; + const chartData = { + labels: libraries, + datasets: scalaVersions.flatMap(scalaVersion => { + return inlinedImplicits.map(inlined => { + const data = libraries.map(library => { + const rowData = rows.find( + row => + row[0] === library && + row[1] === scalaVersion && + row[2] === inlined + ); + return rowData + ? selectedView === 'avg' + ? parseFloat(rowData[3]) + : parseFloat(rowData[4]) + : 0; }); - }).flat(), - }; + return { + label: (inlinedImplicits.length === 1) ? scalaVersion : `${scalaVersion} - Inlined: ${inlined}`, + backgroundColor: generateColor(scalaVersion, inlined), + data: data, + }; + }); + }).flat(), + }; - const ctx = document.getElementById(id).getContext('2d'); - const newChartInstance = new Chart(ctx, { - type: 'bar', - data: chartData, - options: { - responsive: true, - maintainAspectRatio: false, - legend: { - position: 'top', - }, - title: { - display: true, - text: - view === 'avg' - ? 'Average Compile Times (Seconds)' - : 'Minimum Compile Times (Seconds)', - }, - scales: { - y: { - stacked: false, - ticks: { - beginAtZero: true, - // callback: value => value + 's', - }, - title: { - display: true, - text: 'Milliseconds', - }, + const ctx = document.getElementById(id).getContext('2d'); + const newChartInstance = new Chart(ctx, { + type: 'bar', + data: chartData, + options: { + responsive: true, + maintainAspectRatio: false, + legend: { + position: 'top', + }, + title: { + display: true, + text: + selectedView === 'avg' + ? 'Average Compile Times (Seconds)' + : 'Minimum Compile Times (Seconds)', + }, + scales: { + y: { + stacked: false, + ticks: { + beginAtZero: true, + // callback: value => value + 's', + }, + title: { + display: true, + text: 'Milliseconds', }, }, }, - }); - setChartInstance(newChartInstance); - }; - - updateChart(selectedView); + }, + }); + setChartInstance(newChartInstance); + // Clean up on unmount return () => { - if (chartInstance) { - chartInstance.destroy(); + if (newChartInstance) { + newChartInstance.destroy(); } }; - }, [selectedView]); + }, [selectedView, csvData, id]); const handleViewChange = event => { setSelectedView(event.target.value); diff --git a/site/src/components/TestingShowcase/index.js b/site/src/components/TestingShowcase/index.js new file mode 100644 index 0000000000..b30ff62661 --- /dev/null +++ b/site/src/components/TestingShowcase/index.js @@ -0,0 +1,142 @@ +import React from "react"; +import CodeBlock from "@theme/CodeBlock"; +import styles from "./styles.module.css"; + +export default function TestingShowcase() { + return ( +
+
+
+

+ Two Testing Superpowers in One Tool +

+

+ Choose your testing approach: lightning-fast in-memory stubs for rapid iteration, + or database-backed tests with automatic test data generation that eliminates the + "lingering state" problem every team struggles with. +

+
+ +
+
+
+ 01 +

In-Memory Testing That Actually Works

+
+

+ Unlike other ORMs, Typo's generated stubs aren't just dumb maps. + They support the full DSL, including joins, filtering, and ordering. +

+ +{`// No database connection needed! +class UserServiceTest extends FunSuite { + test("find active premium users") { + val users = UserRepoMock.empty + val subs = SubscriptionRepoMock.empty + + // Insert test data + users.insertMany(testUsers) + subs.insertMany(testSubscriptions) + + // Your actual business logic works! + val premiumUsers = users.select + .where(_.active === true) + .join(subs.select) + .on((u, s) => u.id === s.userId) + .where(_._2.plan === "premium") + .map(_._1) + + // Runs instantly! + assert(premiumUsers.size == 3) + } +}`} + +
+ +
+
+ 02 +

Database Tests with Zero Background State

+
+

+ TestInsert generates random data for all fields, but you override exactly + what you care about. You're only forced to provide foreign keys, so you naturally + build valid data graphs with the right shapes. +

+ +{`// All data is random except what you specify +test("user with specific email domain") { + val insert = new TestInsert(new Random(0)) + + // Random name, phone, etc. Only email matters + val user = insert.user(email = "test@company.com") + + // Random category data, only need the ID + val category = insert.category() + + // Random product data, but specific price + val product = insert.product( + categoryId = category.id, // forced - builds valid graph + price = BigDecimal("19.99") // only what we care about + ) + + // Test the specific scenario + val order = orderService.createOrder(user.id, product.id) + + // No accidental dependencies on random data! + assert(order.userEmail == "test@company.com") + assert(order.total == BigDecimal("19.99")) +} + +// Valid foreign keys enforced! +// Everything else is random! +// Test exactly what matters!`} + +
+ +
+
+ 03 +

Property-Based Testing Support

+
+

+ Combine Typo with ScalaCheck for powerful property-based tests. + Generate arbitrary valid data that respects your schema constraints. +

+ +{`// Typo + ScalaCheck = ❤️ +forAll(genValidUser) { user => + val repo = UserRepoMock.empty + repo.insert(user) + + // Properties always hold + repo.selectById(user.id) should contain(user) + repo.selectAll.size shouldBe 1 + + // Even complex properties! + repo.select + .where(_.email === user.email) + .head.id shouldBe user.id +}`} + +
+
+ +
+
+
1000x
+
Faster than DB tests
+
+
+
100%
+
DSL compatibility
+
+
+
0
+
External dependencies
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/site/src/components/TestingShowcase/styles.module.css b/site/src/components/TestingShowcase/styles.module.css new file mode 100644 index 0000000000..4fbbf4948a --- /dev/null +++ b/site/src/components/TestingShowcase/styles.module.css @@ -0,0 +1,128 @@ +.testing { + padding: 5rem 0; + background: var(--ifm-background-surface-color); +} + +.header { + text-align: center; + margin-bottom: 4rem; +} + +.title { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 1rem; + background: var(--typo-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + font-size: 1.25rem; + color: var(--ifm-font-color-secondary); + max-width: 700px; + margin: 0 auto; + line-height: 1.6; +} + +.showcaseGrid { + display: grid; + grid-template-columns: 1fr; + gap: 3rem; + margin-bottom: 4rem; +} + +.showcaseItem { + background: var(--ifm-card-background-color); + border-radius: 12px; + padding: 2rem; + border: 1px solid var(--ifm-color-emphasis-200); + animation: fadeInUp 0.8s ease-out both; +} + +.showcaseItem:nth-child(1) { animation-delay: 0.1s; } +.showcaseItem:nth-child(2) { animation-delay: 0.2s; } +.showcaseItem:nth-child(3) { animation-delay: 0.3s; } + +.showcaseHeader { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.showcaseNumber { + font-size: 1.5rem; + font-weight: 800; + color: var(--ifm-color-primary); + opacity: 0.3; +} + +.showcaseHeader h3 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; +} + +.showcaseDescription { + font-size: 1.1rem; + line-height: 1.6; + margin-bottom: 1.5rem; + color: var(--ifm-font-color-secondary); +} + +.showcaseCode { + font-size: 0.85rem; +} + +.showcaseCode :global(.theme-code-block) { + margin: 0; +} + +.stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + text-align: center; + margin-top: 4rem; +} + +.stat { + animation: fadeIn 0.8s ease-out 0.5s both; +} + +.statNumber { + font-size: 3rem; + font-weight: 800; + color: var(--ifm-color-primary); + margin-bottom: 0.5rem; +} + +.statLabel { + font-size: 1.1rem; + color: var(--ifm-font-color-secondary); +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .stats { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .showcaseHeader { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/site/src/components/TypeSafetyDemo/index.js b/site/src/components/TypeSafetyDemo/index.js new file mode 100644 index 0000000000..fe510c3b98 --- /dev/null +++ b/site/src/components/TypeSafetyDemo/index.js @@ -0,0 +1,67 @@ +import React from "react"; +import CodeBlock from "@theme/CodeBlock"; +import styles from "./styles.module.css"; + +export default function TypeSafetyDemo() { + return ( +
+
+

See Type Safety in Action

+
+
+

PostgreSQL Schema

+
+ +{`CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL, + name VARCHAR(100) +); + +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL + REFERENCES users(id), + title VARCHAR(200) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +);`} + +
+
+
+ + Typo generates +
+
+

Type-Safe Scala Code

+
+ +{`// Strongly typed ID types +case class UserId(value: Long) +case class PostId(value: Long) + +// Row classes with proper types +case class UserRow( + id: UserId, + email: String, + name: Option[String] +) + +case class PostRow( + id: PostId, + userId: UserId, // Not just Long! + title: String, + createdAt: TypoLocalDateTime +) + +// Type-safe repositories +UserRepo.selectById(UserId(42)) +PostRepo.selectByUserId(userId)`} + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/site/src/components/TypeSafetyDemo/styles.module.css b/site/src/components/TypeSafetyDemo/styles.module.css new file mode 100644 index 0000000000..13a017db02 --- /dev/null +++ b/site/src/components/TypeSafetyDemo/styles.module.css @@ -0,0 +1,168 @@ +.demo { + padding: 5rem 0; + background: linear-gradient(135deg, var(--ifm-background-color) 0%, var(--ifm-background-surface-color) 100%); + position: relative; + overflow: hidden; +} + +.demo::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 100%; + height: 100%; + background: radial-gradient(circle, var(--ifm-color-primary-lightest) 0%, transparent 70%); + opacity: 0.05; + animation: rotate 40s linear infinite reverse; +} + +.demoTitle { + text-align: center; + font-size: 2.5rem; + margin-bottom: 3rem; + color: var(--ifm-heading-color); + animation: fadeIn 0.8s ease-out; +} + +.demoContainer { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 2rem; + align-items: start; + animation: fadeIn 1s ease-out 0.2s both; +} + +.sqlSide, .scalaSide { + animation: slideIn 0.8s ease-out 0.4s both; +} + +.sqlSide { + animation-name: slideInLeft; +} + +.scalaSide { + animation-name: slideInRight; +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.sqlSide h3, .scalaSide h3 { + margin-bottom: 1rem; + color: var(--ifm-color-primary); + font-size: 1.3rem; +} + +.codeWrapper { + border-radius: 8px; + overflow: hidden; + box-shadow: var(--typo-shadow-md); + transition: all var(--typo-transition-normal); +} + +.codeWrapper:hover { + box-shadow: var(--typo-shadow-lg); + transform: translateY(-2px); +} + +/* Override default CodeBlock styles for better integration */ +.codeWrapper :global(.theme-code-block) { + margin: 0; +} + +.codeWrapper :global(.theme-code-block pre) { + font-size: 0.85rem; + line-height: 1.6; +} + +.arrow { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 2rem; + color: var(--ifm-color-primary); + animation: pulse 2s ease-in-out infinite; +} + +.arrowText { + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +/* Syntax highlighting simulation */ +.codeBlock code::selection { + background: var(--ifm-color-primary-lighter); + color: white; +} + +/* Responsive design */ +@media (max-width: 1200px) { + .demoContainer { + grid-template-columns: 1fr; + gap: 3rem; + } + + .arrow { + transform: rotate(90deg); + margin: -1rem 0; + } + + .arrowText { + transform: rotate(-90deg); + } +} + +@media (max-width: 768px) { + .demoTitle { + font-size: 2rem; + } + + .codeBlock { + padding: 1rem; + font-size: 0.8rem; + } +} + +/* Dark mode adjustments */ +[data-theme='dark'] .codeBlock { + background: var(--typo-gray-100); + border-color: var(--typo-gray-300); +} + +[data-theme='dark'] .demo::before { + opacity: 0.03; +} \ No newline at end of file diff --git a/site/src/components/TypoLogo/index.js b/site/src/components/TypoLogo/index.js new file mode 100644 index 0000000000..fcc10df06b --- /dev/null +++ b/site/src/components/TypoLogo/index.js @@ -0,0 +1,29 @@ +import React from "react"; +import styles from "./styles.module.css"; + +export default function TypoLogo({ size = 100, animated = true }) { + return ( +
+ + + + + + + + + + {/* Simple abstract shape - made bigger */} + + + +
+ ); +} \ No newline at end of file diff --git a/site/src/components/TypoLogo/styles.module.css b/site/src/components/TypoLogo/styles.module.css new file mode 100644 index 0000000000..11fafd6ed9 --- /dev/null +++ b/site/src/components/TypoLogo/styles.module.css @@ -0,0 +1,14 @@ +.logoContainer { + display: inline-block; + position: relative; +} + +.animatedLogo { + width: 100%; + height: 100%; +} + +.staticLogo { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/site/src/components/WhyTypo/index.js b/site/src/components/WhyTypo/index.js new file mode 100644 index 0000000000..38a2529943 --- /dev/null +++ b/site/src/components/WhyTypo/index.js @@ -0,0 +1,60 @@ +import React from "react"; +import Link from "@docusaurus/Link"; +import styles from "./styles.module.css"; + +export default function WhyTypo() { + return ( +
+
+

+ Why Teams Choose Typo Over Everything Else +

+ +
+
+

vs. Traditional ORMs

+
    +
  • Zero complexity debt - No entity managers, session state, or lazy loading issues
  • +
  • Predictable performance - No N+1 queries or hidden roundtrips
  • +
  • SQL-first - Use the full power of PostgreSQL, not a subset
  • +
  • Debuggable - See exactly what SQL runs, no abstraction layers
  • +
+
+ +
+

vs. Writing SQL by Hand

+
    +
  • Zero boilerplate - No manual mapping code ever
  • +
  • Type safety - Catch errors at compile time, not runtime
  • +
  • Faster compilation - No runtime reflection or macro magic
  • +
  • Automatic updates - Schema changes = instant code updates
  • +
+
+ +
+

vs. JOOQ

+
    +
  • Stronger type safety - Specific ID types and proper nullability with Option[T]
  • +
  • Open source - No commercial licensing headaches
  • +
  • Scala-native - Idiomatic code, not Java translations
  • +
  • PostgreSQL-focused - Deep integration, not generic
  • +
+
+
+ +
+

Ready to Ship Faster with Fewer Bugs?

+

Join developers who've discovered the joy of type-safe database development.

+
+ + Get Started Now + + + Read the Docs + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/site/src/components/WhyTypo/styles.module.css b/site/src/components/WhyTypo/styles.module.css new file mode 100644 index 0000000000..bd28a7690a --- /dev/null +++ b/site/src/components/WhyTypo/styles.module.css @@ -0,0 +1,136 @@ +.whyTypo { + padding: 5rem 0; + background: var(--ifm-background-color); +} + +.title { + text-align: center; + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 3rem; + color: var(--ifm-heading-color); +} + +.comparisonGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + margin-bottom: 4rem; +} + +.comparisonItem { + background: var(--ifm-card-background-color); + border-radius: 12px; + padding: 2rem; + border: 1px solid var(--ifm-color-emphasis-200); + transition: all var(--typo-transition-normal); +} + +.comparisonItem:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.08); + border-color: var(--ifm-color-primary-lighter); +} + +.comparisonItem h3 { + font-size: 1.3rem; + margin-bottom: 1.5rem; + color: var(--ifm-color-primary); +} + +.benefitsList { + list-style: none; + padding: 0; + margin: 0; +} + +.benefitsList li { + margin-bottom: 1rem; + line-height: 1.6; + color: var(--ifm-font-color-base); +} + +.benefitsList li:last-child { + margin-bottom: 0; +} + +.benefitsList strong { + color: var(--ifm-heading-color); +} + +.testimonial { + background: var(--typo-gradient-primary); + border-radius: 16px; + padding: 3rem; + text-align: center; + margin-bottom: 4rem; + position: relative; + overflow: hidden; +} + +.testimonial::before { + content: '"'; + position: absolute; + top: -20px; + left: 20px; + font-size: 120px; + opacity: 0.1; + color: white; + font-family: serif; +} + +.quote { + font-size: 1.5rem; + line-height: 1.6; + color: white; + margin: 0 auto 1rem; + max-width: 800px; +} + +.quote strong { + font-weight: 800; +} + +.attribution { + color: rgba(255, 255, 255, 0.8); + font-style: italic; + margin: 0; +} + +.bottomCTA { + text-align: center; +} + +.bottomCTA h3 { + font-size: 2rem; + margin-bottom: 1rem; +} + +.bottomCTA p { + font-size: 1.25rem; + color: var(--ifm-font-color-secondary); + margin-bottom: 2rem; +} + +.ctaButtons { + display: flex; + gap: 1rem; + justify-content: center; +} + +@media (max-width: 1200px) { + .comparisonGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .ctaButtons { + flex-direction: column; + align-items: center; + } + + .quote { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/site/src/css/custom.css b/site/src/css/custom.css index 38591401e3..3e4443f2db 100644 --- a/site/src/css/custom.css +++ b/site/src/css/custom.css @@ -4,27 +4,492 @@ * work well for content-centric websites. */ -/* You can override the default Infima variables here. */ +/* Import beautiful fonts */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap'); + +/* Elegant purple palette inspired by Felis */ :root { - --ifm-color-primary: #2e8555; - --ifm-color-primary-dark: #29784c; - --ifm-color-primary-darker: #277148; - --ifm-color-primary-darkest: #205d3b; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; + /* Primary colors - Rich purple */ + --ifm-color-primary: #8b5cf6; + --ifm-color-primary-dark: #7c3aed; + --ifm-color-primary-darker: #6d28d9; + --ifm-color-primary-darkest: #5b21b6; + --ifm-color-primary-light: #a78bfa; + --ifm-color-primary-lighter: #c4b5fd; + --ifm-color-primary-lightest: #ddd6fe; + + /* Accent colors - Emerald green */ + --typo-accent: #10b981; + --typo-accent-light: #34d399; + --typo-accent-dark: #059669; + + /* Database grays */ + --typo-gray-900: #1a1a1a; + --typo-gray-800: #2d2d2d; + --typo-gray-700: #404040; + --typo-gray-600: #525252; + --typo-gray-500: #737373; + --typo-gray-400: #999999; + --typo-gray-300: #bfbfbf; + --typo-gray-200: #e6e6e6; + --typo-gray-100: #f5f5f5; + + /* Typography - Beautiful fonts */ + --ifm-font-family-base: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --ifm-font-family-monospace: "JetBrains Mono", "Fira Code", "Consolas", monospace; --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ifm-heading-font-weight: 800; + --ifm-font-weight-semibold: 600; + --ifm-font-weight-bold: 700; + --ifm-line-height-base: 1.65; + + /* Spacing */ + --ifm-spacing-horizontal: 1.5rem; + --ifm-spacing-vertical: 1.5rem; + + /* Borders and Shadows */ + --ifm-global-radius: 8px; + --typo-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --typo-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --typo-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --typo-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* Transitions */ + --typo-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --typo-transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); + --typo-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); + + /* Code highlighting */ + --docusaurus-highlighted-code-line-bg: rgba(51, 103, 145, 0.15); + + /* Gradients */ + --typo-gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --typo-gradient-accent: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + --typo-gradient-magic: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + + /* Background colors */ + --typo-hero-bg-light: #faf5ff; + --typo-hero-bg-dark: #1e1b4b; + + /* Code block colors */ + --typo-code-keyword: #8b5cf6; + --typo-code-string: #10b981; + --typo-code-function: #3b82f6; } -/* For readability concerns, you should choose a lighter palette in dark mode. */ +/* Dark mode with elegant contrast */ [data-theme="dark"] { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + /* Primary colors - Lighter purple for dark mode */ + --ifm-color-primary: #a78bfa; + --ifm-color-primary-dark: #9333ea; + --ifm-color-primary-darker: #7e22ce; + --ifm-color-primary-darkest: #6b21a8; + --ifm-color-primary-light: #c4b5fd; + --ifm-color-primary-lighter: #ddd6fe; + --ifm-color-primary-lightest: #ede9fe; + + /* Accent colors - Brighter green for dark mode */ + --typo-accent: #34d399; + --typo-accent-light: #6ee7b7; + --typo-accent-dark: #10b981; + + /* Dark mode grays */ + --typo-gray-900: #f5f5f5; + --typo-gray-800: #e6e6e6; + --typo-gray-700: #bfbfbf; + --typo-gray-600: #999999; + --typo-gray-500: #737373; + --typo-gray-400: #525252; + --typo-gray-300: #404040; + --typo-gray-200: #2d2d2d; + --typo-gray-100: #1a1a1a; + + /* Dark mode specific */ + --ifm-background-color: #0f0f0f; + --ifm-background-surface-color: #1a1a1a; + --docusaurus-highlighted-code-line-bg: rgba(79, 155, 217, 0.2); + + /* Dark mode gradients */ + --typo-gradient-primary: linear-gradient(135deg, #a78bfa 0%, #c4b5fd 100%); + --typo-gradient-accent: linear-gradient(135deg, #a78bfa 0%, #34d399 100%); + --typo-gradient-magic: linear-gradient(135deg, #c4b5fd 0%, #34d399 100%); + + /* Enhanced shadows for dark mode */ + --typo-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --typo-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --typo-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --typo-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.4); +} + +/* Global animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +@keyframes glow { + 0%, 100% { + box-shadow: 0 0 5px var(--ifm-color-primary), 0 0 10px var(--ifm-color-primary); + } + 50% { + box-shadow: 0 0 20px var(--ifm-color-primary), 0 0 30px var(--ifm-color-primary); + } +} + +@keyframes typewriter { + from { + width: 0; + } + to { + width: 100%; + } +} + +@keyframes blink { + 0%, 50% { + opacity: 1; + } + 51%, 100% { + opacity: 0; + } +} + +/* Enhanced typography */ +.hero__title { + font-weight: 900; + letter-spacing: -0.02em; + animation: fadeIn 0.8s ease-out; +} + +/* Subtle sparkle effect for special elements */ +.sparkle { + position: relative; +} + +.sparkle::after { + content: '✨'; + position: absolute; + top: -0.5em; + right: -1.5em; + font-size: 0.7em; + animation: sparkle 3s ease-in-out infinite; +} + +@keyframes sparkle { + 0%, 100% { + opacity: 0; + transform: scale(0) rotate(0deg); + } + 50% { + opacity: 1; + transform: scale(1) rotate(180deg); + } +} + +.hero__subtitle { + animation: fadeIn 1s ease-out 0.2s both; +} + +/* Button enhancements */ +.button { + position: relative; + overflow: hidden; + transition: all var(--typo-transition-normal); + font-weight: 600; +} + +.button::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.button:hover::before { + width: 300px; + height: 300px; +} + +.button--primary { + background: var(--ifm-color-primary); + border: none; + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.2); + font-weight: 600; +} + +.button--primary:hover { + background: var(--ifm-color-primary-dark); + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); + transform: translateY(-1px); +} + +.button--outline { + border: 2px solid var(--ifm-color-primary); + background: transparent; + color: var(--ifm-color-primary); + font-weight: 600; +} + +.button--outline:hover { + background: var(--ifm-color-primary); + color: white; + transform: translateY(-1px); +} + +/* Card animations */ +.card { + transition: all var(--typo-transition-normal); + box-shadow: var(--typo-shadow-md); +} + +.card:hover { + transform: translateY(-5px); + box-shadow: var(--typo-shadow-xl); +} + +/* Navigation enhancements */ +.navbar__link { + position: relative; + transition: all var(--typo-transition-fast); +} + +.navbar__link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: var(--ifm-color-primary); + transition: width var(--typo-transition-normal); + border-radius: 2px; +} + +.navbar__link:hover::after, +.navbar__link--active::after { + width: 100%; +} + +/* Code block enhancements */ +.theme-code-block { + font-family: var(--ifm-font-family-monospace); +} + +.theme-code-block pre { + font-family: var(--ifm-font-family-monospace); +} + +.theme-code-block pre code { + font-family: var(--ifm-font-family-monospace); +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Feature cards animation */ +.features_src-components-HomepageFeatures-styles-module { + animation: fadeIn 0.8s ease-out 0.4s both; +} + +/* Enhanced links */ +a { + position: relative; + transition: color var(--typo-transition-fast); +} + +article a:not(.hash-link):hover { + color: var(--ifm-color-primary-darker); +} + +/* Table enhancements */ +table { + animation: fadeIn 0.5s ease-out; + box-shadow: var(--typo-shadow-sm); + border-radius: var(--ifm-global-radius); + overflow: hidden; +} + +table thead { + background: var(--typo-gradient-primary); + color: white; +} + +[data-theme="dark"] table thead { + background: var(--typo-gray-200); + color: var(--typo-gray-900); +} + +/* Hero section background animation */ +.hero { + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, var(--ifm-color-primary-lightest) 0%, transparent 70%); + opacity: 0.1; + animation: rotate 30s linear infinite; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Badge styling */ +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + font-size: 0.875rem; + font-weight: 600; + line-height: 1; + color: white; + background: var(--typo-gradient-accent); + border-radius: 9999px; + animation: pulse 2s infinite; +} + +/* Loading animation for async content */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid var(--ifm-color-primary-lightest); + border-radius: 50%; + border-top-color: var(--ifm-color-primary); + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Hover effects for interactive elements */ +.interactive { + transition: all var(--typo-transition-normal); + cursor: pointer; +} + +.interactive:hover { + transform: scale(1.02); +} + +/* Sidebar enhancements */ +.menu__link { + transition: all var(--typo-transition-fast); + border-left: 3px solid transparent; +} + +.menu__link:hover { + background-color: var(--ifm-menu-color-background-hover); + border-left-color: var(--ifm-color-primary); +} + +.menu__link--active { + border-left-color: var(--ifm-color-primary); + font-weight: 600; +} + +/* Footer enhancements */ +.footer { + background: linear-gradient(to bottom, var(--ifm-background-surface-color), var(--ifm-background-color)); +} + +.footer__links { + animation: fadeIn 0.8s ease-out; +} + +/* Navbar logo customization */ +.navbar__logo { + height: 2rem; + width: 2rem; + margin-right: 0.5rem; +} + +.navbar__logo .logoContainer { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +/* Ensure TypoLogo scales properly in navbar */ +.navbar__logo svg { + height: 100%; + width: 100%; +} + +/* Responsive adjustments */ +@media (max-width: 996px) { + :root { + --ifm-spacing-horizontal: 1rem; + --ifm-spacing-vertical: 1rem; + } +} + + +/* Performance fix - disable heavy animations causing high CPU */ +@media screen { + /* Disable the heaviest animations only on specific components */ + .floatingShape, + .gridOverlay, + .animatedBackground, + .magicalWizard *, + .wizardContainer * { + animation: none \!important; + transition: none \!important; + } + + /* Remove performance-heavy filters */ + .floatingShape, + .animatedBackground * { + filter: none \!important; + } } diff --git a/site/src/pages/index.js b/site/src/pages/index.js index f518b32a3c..d685469559 100644 --- a/site/src/pages/index.js +++ b/site/src/pages/index.js @@ -3,27 +3,65 @@ import clsx from "clsx"; import Link from "@docusaurus/Link"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; +import CodeBlock from "@theme/CodeBlock"; import HomepageFeatures from "@site/src/components/HomepageFeatures"; +import FeatureShowcase from "@site/src/components/FeatureShowcase"; +import WhyTypo from "@site/src/components/WhyTypo"; +import TypoLogo from "@site/src/components/TypoLogo"; import styles from "./index.module.css"; function HomepageHeader() { const {siteConfig} = useDocusaurusContext(); return ( -
+
+
+
+
+
+
+
-

{siteConfig.title}

-

{siteConfig.tagline}

-
-
- - Read full introduction - -
-
- - Get started - +
+
+
+ +

Typo

+
+ +

+ The Scala + PostgreSQL toolkit that +
+ brings your database into your type system. +

+ +

+ Typo revolutionizes database development with unprecedented type safety, + a fantastic testing story, and a DSL so intuitive it feels like cheating. + Your database schema becomes your type system. Your domain model stays in sync automatically. +

+ +
+
+ Fantastic testing support - Both in-memory stubs and database test helpers +
+
+ Composite keys done right - First-class support with type-safe helpers +
+
+ "It just works" - Complex queries work correctly the first time +
+
+ +
+ + Start Building in 2 Minutes → + + + See the Magic + +
+
@@ -43,6 +81,8 @@ export default function Home() {
+ +
); diff --git a/site/src/pages/index.module.css b/site/src/pages/index.module.css index 9f71a5da77..f7a156b49d 100644 --- a/site/src/pages/index.module.css +++ b/site/src/pages/index.module.css @@ -4,20 +4,524 @@ */ .heroBanner { - padding: 4rem 0; + padding: 4rem 0 6rem; + position: relative; + overflow: hidden; + background: var(--typo-hero-bg-light); +} + +[data-theme="dark"] .heroBanner { + background: var(--typo-hero-bg-dark); +} + +/* Enhanced animated background */ +.animatedBackground { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + pointer-events: none; +} + +.floatingShape { + position: absolute; + border-radius: 50%; + background: var(--typo-gradient-primary); + opacity: 0.03; + filter: blur(30px); + animation: floatShape 20s ease-in-out infinite; +} + +.floatingShape:nth-child(1) { + width: 300px; + height: 300px; + top: -10%; + left: -5%; + animation-delay: 0s; + animation-duration: 18s; +} + +.floatingShape:nth-child(2) { + width: 200px; + height: 200px; + top: 60%; + right: -10%; + animation-delay: -5s; + animation-duration: 22s; +} + +.floatingShape:nth-child(3) { + width: 150px; + height: 150px; + top: 30%; + left: 80%; + animation-delay: -10s; + animation-duration: 25s; +} + +.gridOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + linear-gradient(rgba(139, 92, 246, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(139, 92, 246, 0.02) 1px, transparent 1px); + background-size: 50px 50px; + animation: gridFloat 30s ease-in-out infinite; +} + +@keyframes floatShape { + 0%, 100% { + transform: translateY(0px) translateX(0px) rotate(0deg) scale(1); + } + 25% { + transform: translateY(-30px) translateX(20px) rotate(90deg) scale(1.1); + } + 50% { + transform: translateY(-10px) translateX(-20px) rotate(180deg) scale(0.9); + } + 75% { + transform: translateY(20px) translateX(10px) rotate(270deg) scale(1.05); + } +} + +@keyframes gridFloat { + 0%, 100% { + transform: translateX(0) translateY(0); + } + 50% { + transform: translateX(10px) translateY(-10px); + } +} + +.heroBanner::before { + content: ''; + position: absolute; + top: -50%; + right: -20%; + width: 80%; + height: 150%; + background: var(--typo-gradient-primary); + opacity: 0.02; + animation: floatBackground 20s ease-in-out infinite; + border-radius: 50%; + filter: blur(60px); +} + +@keyframes floatBackground { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 50% { + transform: translateY(-50px) rotate(180deg); + } +} + +.heroGrid { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; text-align: center; + max-width: 800px; + margin: 0 auto; +} + +.heroLeft { + animation: fadeInUp 0.8s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.logoSection { + display: flex; + align-items: center; + gap: 1.5rem; + margin-bottom: 2rem; + justify-content: center; +} + +.brandName { + font-size: 3.5rem; + font-weight: 900; + margin: 0; + background: var(--typo-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: -0.02em; +} + +.heroHeadline { + font-size: 2.5rem; + font-weight: 800; + line-height: 1.2; + margin-bottom: 1.5rem; + color: var(--ifm-heading-color); +} + +.highlight { + background: var(--typo-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 800; +} + +.heroSubtext { + font-size: 1.25rem; + line-height: 1.6; + color: var(--ifm-font-color-secondary); + margin-bottom: 2rem; +} + +.valueProps { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; + align-items: center; +} + +.valueProp { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 1.1rem; + animation: fadeInUp 0.8s ease-out both; +} + +.valueProp:nth-child(1) { animation-delay: 0.3s; } +.valueProp:nth-child(2) { animation-delay: 0.4s; } +.valueProp:nth-child(3) { animation-delay: 0.5s; } + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.valueIcon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.valueText { + color: var(--ifm-font-color-base); +} + +.valueText strong { + color: var(--ifm-color-primary); + font-weight: 700; +} + +.buttonsContainer { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + animation: fadeInUp 0.8s ease-out 0.6s both; + justify-content: center; +} + +.buttonsContainer .button { position: relative; overflow: hidden; + z-index: 1; + transition: all var(--typo-transition-normal); } -@media screen and (max-width: 996px) { +.buttonsContainer .button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left var(--typo-transition-normal); + z-index: -1; +} + +.buttonsContainer .button:hover::before { + left: 100%; +} + +.buttonsContainer .button--primary { + background: var(--typo-gradient-primary); + border: none; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3); +} + +.buttonsContainer .button--primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4); +} + +.buttonsContainer .button--outline { + border: 2px solid var(--ifm-color-primary); + color: var(--ifm-color-primary); + background: transparent; + position: relative; +} + +.buttonsContainer .button--outline:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(139, 92, 246, 0.2); + background: var(--ifm-color-primary); + color: white; +} + +.socialProof { + display: flex; + gap: 2rem; + font-size: 0.9rem; + color: var(--ifm-font-color-secondary); + animation: fadeInUp 0.8s ease-out 0.7s both; + justify-content: center; +} + +.proofItem { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.proofIcon { + font-size: 1.1rem; +} + +.proofDivider { + color: var(--ifm-color-emphasis-500); + margin: 0 0.25rem; +} + +/* Demo section */ +.demoContainer { + background: var(--ifm-background-surface-color); + border-radius: 16px; + box-shadow: 0 20px 40px rgba(139, 92, 246, 0.08); + overflow: hidden; + border: 1px solid var(--ifm-color-emphasis-200); + backdrop-filter: blur(10px); +} + +.demoHeader { + background: var(--typo-gradient-primary); + padding: 1rem 1.5rem; + text-align: center; +} + +.demoTitle { + color: white; + font-weight: 600; + font-size: 1.1rem; +} + +.demoContent { + padding: 2rem; +} + +.demoFeature { + margin-bottom: 1.5rem; +} + +.demoFeature:last-child { + margin-bottom: 0; +} + +.demoFeature h4 { + font-size: 1.1rem; + margin-bottom: 0.75rem; + color: var(--ifm-heading-color); + font-weight: 700; +} + +.stepNumber { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--ifm-color-primary); + color: white; + font-weight: 600; + flex-shrink: 0; + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3); +} + +.stepContent { + flex: 1; +} + +.stepLabel { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--ifm-heading-color); +} + +.miniCode { + font-size: 0.8rem !important; +} + +.miniCode :global(.theme-code-block) { + margin: 0; +} + +.miniCode :global(.theme-code-block pre) { + padding: 0.75rem; + font-size: 0.8rem; + line-height: 1.4; +} + +.demoArrow { + text-align: center; + font-size: 2rem; + color: var(--ifm-color-primary); + margin: 1rem 0; + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +.demoFooter { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--ifm-color-emphasis-200); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.errorExample { + font-size: 0.9rem; + color: var(--ifm-color-danger); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.errorExample code { + background: var(--ifm-color-danger-lightest); + color: var(--ifm-color-danger-darkest); + padding: 0.2rem 0.4rem; + border-radius: 4px; +} + +/* Dark mode adjustments */ +[data-theme="dark"] .demoContainer { + background: var(--typo-gray-200); + border-color: var(--typo-gray-300); +} + +[data-theme="dark"] .highlight { + background: linear-gradient(135deg, var(--ifm-color-primary-light) 0%, var(--typo-accent-light) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +[data-theme="dark"] .brandName { + background: linear-gradient(135deg, var(--ifm-color-primary-light) 0%, var(--ifm-color-primary-lighter) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Responsive design */ +@media screen and (max-width: 1200px) { + .heroGrid { + grid-template-columns: 1fr; + gap: 3rem; + } + + .heroRight { + max-width: 600px; + margin: 0 auto; + } +} + +@media screen and (max-width: 768px) { .heroBanner { - padding: 2rem; + padding: 3rem 0 4rem; + } + + .logoSection { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .brandName { + font-size: 2.5rem; + } + + .heroHeadline { + font-size: 2rem; + } + + .heroSubtext { + font-size: 1.1rem; + } + + .valueProps { + gap: 0.75rem; + } + + .valueProp { + font-size: 1rem; + } + + .buttonsContainer { + flex-direction: column; + align-items: flex-start; + } + + .socialProof { + flex-wrap: wrap; + gap: 1rem; } } +/* Remove old styles */ .buttons { display: flex; align-items: center; justify-content: center; } + +.heroFeatures, +.feature, +.featureIcon, +.featureLabel, +.heroContent, +.titleMain { + /* Removed - no longer used */ +} \ No newline at end of file diff --git a/site/src/theme/Logo/index.js b/site/src/theme/Logo/index.js new file mode 100644 index 0000000000..890129d2c4 --- /dev/null +++ b/site/src/theme/Logo/index.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import ThemedImage from '@theme/ThemedImage'; +import TypoLogo from '@site/src/components/TypoLogo'; + +function LogoThemedImage({logo, alt, imageClassName}) { + const sources = { + light: useBaseUrl(logo.src), + dark: useBaseUrl(logo.srcDark || logo.src), + }; + const themedImage = ( + + ); + // Is this extra div really necessary? + // introduced in https://github.com/facebook/docusaurus/pull/5666 + return imageClassName ? ( +
{themedImage}
+ ) : ( + themedImage + ); +} + +export default function Logo(props) { + const { + siteConfig: {title}, + } = useDocusaurusContext(); + const { + navbar: {title: navbarTitle, logo}, + } = useThemeConfig(); + + const {imageClassName, titleClassName, ...propsRest} = props; + const logoLink = useBaseUrl(logo?.href || '/'); + + // If they don't have a logo defined, use the TypoLogo + // Otherwise, check if logo.src contains "logo.svg" and replace with TypoLogo + const shouldUseTypoLogo = !logo?.src || logo.src === 'img/logo.svg'; + + const fallbackAlt = navbarTitle ?? title; + const alt = logo?.alt ?? fallbackAlt; + + return ( + + {shouldUseTypoLogo ? ( +
+ +
+ ) : ( + <> + {logo && } + + )} + {navbarTitle != null && {navbarTitle}} + + ); +} \ No newline at end of file diff --git a/site/static/img/favicon.ico b/site/static/img/favicon.ico index c01d54bcd3..bdc76774c6 100644 Binary files a/site/static/img/favicon.ico and b/site/static/img/favicon.ico differ diff --git a/site/static/img/favicon.svg b/site/static/img/favicon.svg new file mode 100644 index 0000000000..dec8a8de5f --- /dev/null +++ b/site/static/img/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/static/img/logo.svg b/site/static/img/logo.svg index 9db6d0d066..a4252d47d6 100644 --- a/site/static/img/logo.svg +++ b/site/static/img/logo.svg @@ -1 +1,16 @@ - \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file diff --git a/typo-scripts/src/scala/scripts/GeneratedFrontpage.scala b/typo-scripts/src/scala/scripts/GeneratedFrontpage.scala new file mode 100644 index 0000000000..c5f9284b7f --- /dev/null +++ b/typo-scripts/src/scala/scripts/GeneratedFrontpage.scala @@ -0,0 +1,58 @@ +package scripts + +import ryddig.{Formatter, LogLevel, LogPatterns, Loggers} +import typo.* +import typo.internal.metadb.OpenEnum +import typo.internal.sqlfiles.readSqlFileDirectories +import typo.internal.{FileSync, generate} + +import java.nio.file.Path +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.Duration + +object GeneratedFrontpage { + val buildDir = Path.of(sys.props("user.dir")) + + // clickable links in intellij + implicit val PathFormatter: Formatter[Path] = _.toUri.toString + + def main(args: Array[String]): Unit = + Loggers + .stdout(LogPatterns.interface(None, noColor = false), disableProgress = true) + .map(_.withMinLogLevel(LogLevel.info)) + .use { logger => + val ds = TypoDataSource.hikari(server = "localhost", port = 6432, databaseName = "Adventureworks", username = "postgres", password = "password") + val scriptsPath = buildDir.resolve("init/data/frontpage") + val selector = Selector.schemas("frontpage") + val typoLogger = TypoLogger.Console + val metadb = Await.result(MetaDb.fromDb(typoLogger, ds, selector, schemaMode = SchemaMode.MultiSchema), Duration.Inf) + val relationNameToOpenEnum = Map.empty[db.RelationName, OpenEnum] + + val sqlScripts = Await.result(readSqlFileDirectories(typoLogger, scriptsPath, ds), Duration.Inf) + + val options = Options( + pkg = "frontpage", + Some(DbLibName.Anorm), + List(JsonLibName.PlayJson), + typeOverride = TypeOverride.Empty, + openEnums = Selector.None, + generateMockRepos = Selector.All, + enablePrimaryKeyType = Selector.All, + enableTestInserts = Selector.All, + readonlyRepo = Selector.None, + enableDsl = true + ) + val targetSources = buildDir.resolve(s"frontpage-generated") + + val newFiles: Generated = + generate(options, metadb, ProjectGraph(name = "", targetSources, None, selector, sqlScripts, Nil), relationNameToOpenEnum).head + + newFiles + .overwriteFolder(softWrite = FileSync.SoftWrite.Yes(Set.empty)) + .filter { case (_, synced) => synced != FileSync.Synced.Unchanged } + .foreach { case (path, synced) => logger.withContext("path", path).warn(synced.toString) } + + logger.info(s"Generated frontpage code to $targetSources") + } +}