diff --git a/.github/workflows/pr-changelog-label-check.yml b/.github/workflows/pr-changelog-label-check.yml index 42c451c..9682635 100644 --- a/.github/workflows/pr-changelog-label-check.yml +++ b/.github/workflows/pr-changelog-label-check.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest permissions: issues: write - pull-requests: read + pull-requests: write steps: - name: Check for required label(s) uses: mheap/github-action-required-labels@v5 diff --git a/README.md b/README.md index b1137f4..5916d67 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +[![Docker Pulls](https://img.shields.io/docker/pulls/johly/airtrail?style=for-the-badge)]() +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/johanohly/AirTrail/build.yml?style=for-the-badge)]() +[![GitHub Release](https://img.shields.io/github/v/release/johanohly/AirTrail?style=for-the-badge)]() +[![Contributors](https://img.shields.io/github/contributors/johanohly/AirTrail?style=for-the-badge)]() +[![GitHub Stars](https://img.shields.io/github/stars/johanohly/AirTrail?style=for-the-badge)]() +
AirTrail logo @@ -17,10 +23,11 @@ - **World Map**: View all your flights on an interactive world map. - **Flight History**: Keep track of all your flights in one place. - **Statistics**: Get insights into your flight history with statistics. -- **User Authentication**: Allow multiple users and secure your data with user authentication. +- **Multiple Users**: Manage multiple users, share flights among them, secure your data with user authentication and + integrate with your OAuth provider. - **Responsive Design**: Use the application on any device with a responsive design. - **Dark Mode**: Switch between light and dark mode. -- **Import Flights**: Import flights from various sources. +- **Import Flights**: Import flights from various sources including MyFlightRadar24, App in the Air and JetLog. ## 🚀 Getting Started diff --git a/bun.lockb b/bun.lockb index 8ece347..8435516 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/docs/features/export.md b/docs/docs/features/export.md new file mode 100644 index 0000000..c7c6045 --- /dev/null +++ b/docs/docs/features/export.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 3 +--- + +# Export + +The export feature allows you to export your flight data from AirTrail. + +## Export flights + +To export your flights, follow these steps: + +1. Go to the AirTrail application. +2. Go to the settings page. +3. Click on the "Export" tab. +4. Choose your desired export format (CSV or JSON). + +## Export formats + +### CSV + +The CSV export option allows you to export your flights as a CSV file, which can be opened in any spreadsheet +application like Microsoft Excel or Google Sheets. It is a simple and easy-to-use format that can be used to +analyze your flight data. + +#### Format + +The CSV file contains the following columns: + +- `date`: The date of the flight (YYYY-MM-DD format). +- `from`: The IATA code of the departure airport. +- `to`: The IATA code of the arrival airport. +- `departure`: The departure time in ISO 8601 format (if available). +- `arrival`: The arrival time in ISO 8601 format (if available). +- `duration`: The duration of the flight in seconds. +- `flightNumber`: The flight number (if available). +- `flightReason`: The reason for the flight (if provided). +- `airline`: The airline operating the flight (if available). +- `aircraft`: The type of aircraft used (if available). +- `aircraftReg`: The registration number of the aircraft (if available). +- `note`: Any additional notes about the flight. +- `seat`: The type of seat (e.g., window, aisle, etc.). +- `seatNumber`: The seat number (if available). +- `seatClass`: The class of the seat (e.g., economy, business). + +### JSON + +:::tip +The JSON format can be reimported into AirTrail using the import feature. +::: + +The JSON export option provides a more structured format that is ideal for developers or when integrating the data into +other systems. It contains nested objects for each flight and detailed data for each user and their seat information. + +#### Format + +The JSON file follows this structure: + +```json +{ + "users": [ + { + "id": "user_id", + "displayName": "User Name", + "username": "username" + } + ], + "flights": [ + { + "date": "YYYY-MM-DD", + "from": "ICAO_CODE", + "to": "ICAO_CODE", + "departure": "ISO_8601_DATETIME", + "arrival": "ISO_8601_DATETIME", + "duration": flight_duration_in_seconds, + "flightNumber": "FLIGHT_NUMBER", + "flightReason": "FLIGHT_REASON", + "airline": "ICAO_AIRLINE_CODE", + "aircraft": "ICAO_AIRCRAFT_TYPE", + "aircraftReg": "AIRCRAFT_REGISTRATION", + "note": "FLIGHT_NOTE", + "seats": [ + { + "userId": "USER_ID", + "guestName": "GUEST_NAME", + "seat": "SEAT_TYPE", + "seatNumber": "SEAT_NUMBER", + "seatClass": "SEAT_CLASS" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/docs/features/import.md b/docs/docs/features/import.md index 3f22f85..4e07c62 100644 --- a/docs/docs/features/import.md +++ b/docs/docs/features/import.md @@ -1,12 +1,13 @@ --- -sidebar_position: 1 +sidebar_position: 2 --- # Import The import feature allows you to import flight data from other sources into AirTrail. Currently, AirTrail supports importing flights from [MyFlightradar24](https://my.flightradar24.com) -and [App in the Air](https://appintheair.com). +, [App in the Air](https://appintheair.com), [JetLog](https://github.com/pbogre/jetlog) +and [AirTrail JSON files](/docs/features/export). ## Import flights from MyFlightradar24 @@ -22,7 +23,7 @@ Once you have the CSV file, you can import it into AirTrail by following these s 1. Go to the AirTrail application. 2. Go to the settings page. 3. Click on the "Import" tab. -4. Click on the "Choose File" button and select the CSV file you downloaded from MyFlightradar24. +4. Click on the "Select file" button and select the CSV file you downloaded from MyFlightradar24. 5. Click on the "Import" button to start the import process. After the import process is complete, you will see your flights on the map. @@ -41,7 +42,46 @@ Once you have the text file, you can import it into AirTrail by following these 1. Go to the AirTrail application. 2. Go to the settings page. 3. Click on the "Import" tab. -4. Click on the "Choose File" button and select the text file you received from App in the Air. +4. Click on the "Select file" button and select the text file you received from App in the Air. +5. Click on the "Import" button to start the import process. + +After the import process is complete, you will see your flights on the map. + +## Import flights from JetLog + +:::tip +Make sure the file you are importing is called `jetlog.csv`. If it is not, rename it to `jetlog.csv` before importing. +::: + +While logged in to your JetLog account, follow these steps to export your flights: + +1. Go to your JetLog instance. +2. Go to the "Settings" page in the top right corner. +3. Click on the "Export to CSV" button to download your flights as a CSV file. + +Once you have the CSV file, you can import it into AirTrail by following these steps: + +1. Go to the AirTrail application. +2. Go to the settings page. +3. Click on the "Import" tab. +4. Click on the "Select file" button and select the CSV file you downloaded from JetLog. +5. Click on the "Import" button to start the import process. + +After the import process is complete, you will see your flights on the map. + +## Import flights from AirTrail JSON files + +:::tip +Make sure the file you are importing is called `airtrail.json`. If it is not, rename it to `airtrail.json` before +importing. +::: + +Once you have the JSON file, you can import it into AirTrail by following these steps: + +1. Go to the AirTrail application. +2. Go to the settings page. +3. Click on the "Import" tab. +4. Click on the "Select file" button and select the JSON file you want to import. 5. Click on the "Import" button to start the import process. After the import process is complete, you will see your flights on the map. diff --git a/docs/docs/features/oauth.md b/docs/docs/features/oauth.md new file mode 100644 index 0000000..aff2c7a --- /dev/null +++ b/docs/docs/features/oauth.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 4 +--- + +# OAuth Authentication + +AirTrail supports authentication via OpenID Connect (OIDC). +This allows you to use your existing identity provider to authenticate users in AirTrail. + +## Prerequisites + +Before you can configure OAuth authentication in AirTrail, you need to set up an OAuth client in your identity provider. +The specific steps to do this depend on the identity provider you are using, but in general you will need to: + +1. Register a new OIDC/OAuth2 client in your identity provider. +2. Configure the client with the following settings: + - Client type: `Confidential` + - Application type: `Web application` + - Grant type: `Authorization Code` +3. Add the following redirect URI to the client configuration: + - `http://DOMAIN:PORT/login` + +## Configuration + +To configure OAuth authentication in AirTrail, go to the `Settings` page and click on the `OAuth` tab (you need to be an +admin to access this page). +Here you can enter the following settings: + +| Setting | Default | Description | +|---------------|----------------|-----------------------------------------------------------------------------------------------------| +| Enabled | `false` | Whether to enable OAuth authentication. | +| Issuer URL | | The URL of the OIDC issuer (e.g. `https://accounts.google.com/.well-known/openid-configuration`). | +| Client ID | | The client ID of the OAuth client you created in your identity provider. | +| Client Secret | | The client secret of the OAuth client you created in your identity provider. | +| Scope | openid profile | The scopes to send with the request. | +| Auto Register | `true` | Whether to automatically register new users if no existing AirTrail user is found for the username. | +| Auto Login | `false` | Whether to automatically launch the OAuth login flow when a user visits the login page. | \ No newline at end of file diff --git a/docs/docs/features/statistics.md b/docs/docs/features/statistics.md new file mode 100644 index 0000000..94d095a --- /dev/null +++ b/docs/docs/features/statistics.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 1 +--- + +# Statistics + +Get insights into your flying habits with the statistics page. +Statistics are available through the bottom navigation bar. + +## Fields + +:::tip Note +Statistics only take into account flights that have been completed. +::: + +| Field | Description | +|--------------------|----------------------------------------------------------------------| +| Flights | The total number of flights you have completed. | +| Distance | The total distance you have flown. | +| Duration | The total time you have spent flying. | +| Airports | The total number of airports you have visited. | +| Seat Classes | The distribution of flights by seat class (Economy, Business, etc.). | +| Seat Distribution | The distribution of flights by seat (Window, Aisle, etc.). | +| Flight Reason | The distribution of flights by reason (Business, Leisure, etc.). | +| Continents Visited | The amount you have visited each continent. | +| Flights by month | The amount of flights per month. | +| Flights by weekday | The amount of flights per weekday. | diff --git a/package.json b/package.json index bdab385..9d9a2fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "airtrail", - "version": "0.1.1", + "version": "0.2.0", "type": "module", "scripts": { "build": "vite build", @@ -18,12 +18,12 @@ "@layerstack/utils": "^0.0.3", "@lucia-auth/adapter-postgresql": "^3.1.2", "@node-rs/argon2": "^1.8.3", - "@o7/icon": "^0.2.3", + "@o7/icon": "^0.3.9", "@prisma/client": "^5.19.1", "@tanstack/svelte-query": "^5.51.16", "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", - "bits-ui": "^0.21.13", + "bits-ui": "^0.21.15", "clsx": "^2.1.1", "cobe": "^0.6.3", "d3-array": "^3.2.4", @@ -32,18 +32,22 @@ "devalue": "^5.0.0", "formsnap": "^1.0.1", "kysely": "^0.27.4", - "layerchart": "^0.43.8", + "layerchart": "^0.52.1", "lru-cache": "^11.0.0", "lucide-svelte": "^0.437.0", "maplibre-gl": "^4.5.1", "mode-watcher": "^0.4.1", + "openid-client": "^5.7.0", "pg": "^8.12.0", + "semver": "^7.6.3", "svelte-maplibre": "^0.9.11", + "svelte-markdown": "^0.4.1", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.27", "sveltekit-superforms": "^2.16.1", "tailwind-merge": "^2.4.0", "tailwind-variants": "^0.2.1", + "topojson-client": "^3.1.0", "trpc-svelte-query": "^2.1.0", "vaul-svelte": "^0.3.2", "zod": "^3.23.8" @@ -56,10 +60,14 @@ "@sveltejs/adapter-node": "^5.2.2", "@sveltejs/kit": "^2.5.24", "@sveltejs/vite-plugin-svelte": "^3.1.1", + "@types/d3-geo": "^3.1.0", "@types/d3-scale": "^4.0.8", "@types/d3-shape": "^3.1.6", "@types/lru-cache": "^7.10.10", "@types/pg": "^8.11.8", + "@types/semver": "^7.5.8", + "@types/topojson-client": "^3.1.5", + "@types/topojson-specification": "^1.0.5", "autoprefixer": "^10.4.19", "eslint": "^9.8.0", "eslint-config-prettier": "^9.1.0", diff --git a/prisma/migrations/20240925181003_oauth/migration.sql b/prisma/migrations/20240925181003_oauth/migration.sql new file mode 100644 index 0000000..f12f4cb --- /dev/null +++ b/prisma/migrations/20240925181003_oauth/migration.sql @@ -0,0 +1,26 @@ +-- AlterTable +ALTER TABLE "user" + ADD COLUMN "oauth_id" TEXT, + ALTER COLUMN "password" DROP NOT NULL; + +-- CreateTable +CREATE TABLE "app_config" +( + "id" SERIAL NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT false, + "issuer_url" TEXT, + "client_id" TEXT, + "client_secret" TEXT, + "scope" TEXT NOT NULL DEFAULT 'openid profile', + "auto_register" BOOLEAN NOT NULL DEFAULT true, + "auto_login" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "app_config_pkey" PRIMARY KEY ("id") +); + +-- Insert +INSERT INTO "app_config" ("enabled", "scope", "auto_register", + "auto_login") +VALUES (false, 'openid profile', true, + false); + diff --git a/prisma/migrations/20241003080714_visited_countries/migration.sql b/prisma/migrations/20241003080714_visited_countries/migration.sql new file mode 100644 index 0000000..8097493 --- /dev/null +++ b/prisma/migrations/20241003080714_visited_countries/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "visited_country" ( + "id" SERIAL NOT NULL, + "code" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "note" TEXT, + "user_id" TEXT NOT NULL, + + CONSTRAINT "visited_country_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "visited_country_code_user_id_key" ON "visited_country"("code", "user_id"); + +-- AddForeignKey +ALTER TABLE "visited_country" ADD CONSTRAINT "visited_country_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b8a857c..59853eb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,19 +11,36 @@ generator kysely { camelCase = true } +model app_config { + id Int @id @default(autoincrement()) + + /// OIDC + + enabled Boolean @default(false) + issuer_url String? + client_id String? + client_secret String? + scope String @default("openid profile") + auto_register Boolean @default(true) + auto_login Boolean @default(false) +} + model user { - id String @id - username String @unique - password String + id String @id + username String @unique display_name String /// @kyselyType('metric' | 'imperial') unit String + password String? /// @kyselyType('user' | 'admin' | 'owner') role String - sessions session[] - seats seat[] + oauth_id String? + + sessions session[] + seats seat[] + visited_countries visited_country[] } model flight { @@ -70,6 +87,21 @@ model seat { @@unique([flight_id, guest_name], map: "seat_flight_id_guest_name_key") } +model visited_country { + id Int @id @default(autoincrement()) + + /// ISO 3166-1 numeric code + code Int + /// @kyselyType('lived' | 'visited' | 'layover' | 'wishlist') + status String + note String? + + user_id String + user user @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@unique([code, user_id], map: "visited_country_code_user_id_key") +} + model session { id String @id expires_at DateTime @db.Timestamptz diff --git a/scripts/other/Country Data Codes.csv b/scripts/other/Country Data Codes.csv new file mode 100644 index 0000000..06486ee --- /dev/null +++ b/scripts/other/Country Data Codes.csv @@ -0,0 +1,279 @@ +Name,GENC,ISO 3166,Stanag,Internet,Comment +"Afghanistan","AFG","AF|AFG|004","AFG",".af","" +"Akrotiri","XQZ","-","-","-","" +"Albania","ALB","AL|ALB|008","ALB",".al","" +"Algeria","DZA","DZ|DZA|012","DZA",".dz","" +"American Samoa","ASM","AS|ASM|016","ASM",".as","" +"Andorra","AND","AD|AND|020","AND",".ad","" +"Angola","AGO","AO|AGO|024","AGO",".ao","" +"Anguilla","AIA","AI|AIA|660","AIA",".ai","" +"Antarctica","ATA","AQ|ATA|010","ATA",".aq","ISO defines as the territory south of 60 degrees south latitude" +"Antigua and Barbuda","ATG","AG|ATG|028","ATG",".ag","" +"Argentina","ARG","AR|ARG|032","ARG",".ar","" +"Armenia","ARM","AM|ARM|051","ARM",".am","" +"Aruba","ABW","AW|ABW|533","ABW",".aw","" +"Ashmore and Cartier Islands","XAC","-","AUS","-","ISO includes with Australia" +"Australia","AUS","AU|AUS|036","AUS",".au","ISO includes Ashmore and Cartier Islands, Coral Sea Islands" +"Austria","AUT","AT|AUT|040","AUT",".at","" +"Azerbaijan","AZE","AZ|AZE|031","AZE",".az","" +"Bahamas, The","BHS","BS|BHS|044","BHS",".bs","" +"Bahrain","BHR","BH|BHR|048","BHR",".bh","" +"Baker Island","XBK","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Bangladesh","BGD","BD|BGD|050","BGD",".bd","" +"Barbados","BRB","BB|BRB|052","BRB",".bb","" +"Bassas da India","XBI","-","-","-","administered as part of French Southern and Antarctic Lands; no ISO codes assigned" +"Belarus","BLR","BY|BLR|112","BLR",".by","" +"Belgium","BEL","BE|BEL|056","BEL",".be","" +"Belize","BLZ","BZ|BLZ|084","BLZ",".bz","" +"Benin","BEN","BJ|BEN|204","BEN",".bj","" +"Bermuda","BMU","BM|BMU|060","BMU",".bm","" +"Bhutan","BTN","BT|BTN|064","BTN",".bt","" +"Bolivia","BOL","BO|BOL|068","BOL",".bo","" +"Bosnia and Herzegovina","BIH","BA|BIH|070","BIH",".ba","" +"Botswana","BWA","BW|BWA|072","BWA",".bw","" +"Bouvet Island","BVT","BV|BVT|074","BVT",".bv","" +"Brazil","BRA","BR|BRA|076","BRA",".br","" +"British Indian Ocean Territory","IOT","IO|IOT|086","IOT",".io","" +"British Virgin Islands","VGB","VG|VGB|092","VGB",".vg","" +"Brunei","BRN","BN|BRN|096","BRN",".bn","" +"Bulgaria","BGR","BG|BGR|100","BGR",".bg","" +"Burkina Faso","BFA","BF|BFA|854","BFA",".bf","" +"Burma","MMR","MM|MMR|104","MMR",".mm","ISO uses the name Myanmar" +"Burundi","BDI","BI|BDI|108","BDI",".bi","" +"Cabo Verde","CPV","CV|CPV|132","CPV",".cv","" +"Cambodia","KHM","KH|KHM|116","KHM",".kh","" +"Cameroon","CMR","CM|CMR|120","CMR",".cm","" +"Canada","CAN","CA|CAN|124","CAN",".ca","" +"Cayman Islands","CYM","KY|CYM|136","CYM",".ky","" +"Central African Republic","CAF","CF|CAF|140","CAF",".cf","" +"Chad","TCD","TD|TCD|148","TCD",".td","" +"Chile","CHL","CL|CHL|152","CHL",".cl","" +"China","CHN","CN|CHN|156","CHN",".cn","see also Taiwan" +"Christmas Island","CXR","CX|CXR|162","CXR",".cx","" +"Clipperton Island","CPT","-","FYP","-","ISO includes with France" +"Cocos (Keeling) Islands","CCK","CC|CCK|166","AUS",".cc","" +"Colombia","COL","CO|COL|170","COL",".co","" +"Comoros","COM","KM|COM|174","COM",".km","" +"Congo, Democratic Republic of the","COD","CD|COD|180","COD",".cd","formerly Zaire" +"Congo, Republic of the","COG","CG|COG|178","COG",".cg","" +"Cook Islands","COK","CK|COK|184","COK",".ck","" +"Coral Sea Islands","XCS","-","AUS","-","ISO includes with Australia" +"Costa Rica","CRI","CR|CRI|188","CRI",".cr","" +"Cote d'Ivoire","CIV","CI|CIV|384","CIV",".ci","" +"Croatia","HRV","HR|HRV|191","HRV",".hr","" +"Cuba","CUB","CU|CUB|192","CUB",".cu","" +"Curacao","CUW","CW|CUW|531","-",".cw","" +"Cyprus","CYP","CY|CYP|196","CYP",".cy","" +"Czechia","CZE","CZ|CZE|203","CZE",".cz","" +"Denmark","DNK","DK|DNK|208","DNK",".dk","" +"Dhekelia","XXD","-","-","-","" +"Djibouti","DJI","DJ|DJI|262","DJI",".dj","" +"Dominica","DMA","DM|DMA|212","DMA",".dm","" +"Dominican Republic","DOM","DO|DOM|214","DOM",".do","" +"Ecuador","ECU","EC|ECU|218","ECU",".ec","" +"Egypt","EGY","EG|EGY|818","EGY",".eg","" +"El Salvador","SLV","SV|SLV|222","SLV",".sv","" +"Equatorial Guinea","GNQ","GQ|GNQ|226","GNQ",".gq","" +"Eritrea","ERI","ER|ERI|232","ERI",".er","" +"Estonia","EST","EE|EST|233","EST",".ee","" +"Eswatini","SWZ","SZ|SWZ|748","SWZ",".sz","" +"Ethiopia","ETH","ET|ETH|231","ETH",".et","" +"Europa Island","XEU","-","-","-","administered as part of French Southern and Antarctic Lands; no ISO codes assigned" +"Falkland Islands (Islas Malvinas)","FLK","FK|FLK|238","FLK",".fk","" +"Faroe Islands","FRO","FO|FRO|234","FRO",".fo","" +"Fiji","FJI","FJ|FJI|242","FJI",".fj","" +"Finland","FIN","FI|FIN|246","FIN",".fi","" +"France","FRA","FR|FRA|250","FRA",".fr","ISO includes metropolitan France along with the dependencies of Clipperton Island, French Guiana, French Polynesia, French Southern and Antarctic Lands, Guadeloupe, Martinique, Mayotte, New Caledonia, Reunion, Saint Pierre and Miquelon, Wallis and Futuna" +"France, Metropolitan","-","FX|FXX|249","-",".fx","ISO limits to the European part of France" +"French Guiana","GUF","GF|GUF|254","GUF",".gf","" +"French Polynesia","PYF","PF|PYF|258","PYF",".pf","" +"French Southern and Antarctic Lands","ATF","TF|ATF|260","ATF",".tf","GENC does not include the French-claimed portion of Antarctica (Terre Adelie)" +"Gabon","GAB","GA|GAB|266","GAB",".ga","" +"Gambia, The","GMB","GM|GMB|270","GMB",".gm","" +"Gaza Strip","XGZ","PS|PSE|275","PSE",".ps","ISO identifies as Occupied Palestinian Territory" +"Georgia","GEO","GE|GEO|268","GEO",".ge","" +"Germany","DEU","DE|DEU|276","DEU",".de","" +"Ghana","GHA","GH|GHA|288","GHA",".gh","" +"Gibraltar","GIB","GI|GIB|292","GIB",".gi","" +"Glorioso Islands","XGL","-","-","-","administered as part of French Southern and Antarctic Lands; no ISO codes assigned" +"Greece","GRC","GR|GRC|300","GRC",".gr","For its internal communications, the European Union recommends the use of the code EL in lieu of the ISO 3166-2 code of GR" +"Greenland","GRL","GL|GRL|304","GRL",".gl","" +"Grenada","GRD","GD|GRD|308","GRD",".gd","" +"Guadeloupe","GLP","GP|GLP|312","GLP",".gp","" +"Guam","GUM","GU|GUM|316","GUM",".gu","" +"Guatemala","GTM","GT|GTM|320","GTM",".gt","" +"Guernsey","GGY","GG|GGY|831","UK",".gg","" +"Guinea","GIN","GN|GIN|324","GIN",".gn","" +"Guinea-Bissau","GNB","GW|GNB|624","GNB",".gw","" +"Guyana","GUY","GY|GUY|328","GUY",".gy","" +"Haiti","HTI","HT|HTI|332","HTI",".ht","" +"Heard Island and McDonald Islands","HMD","HM|HMD|334","HMD",".hm","" +"Holy See (Vatican City)","VAT","VA|VAT|336","VAT",".va","" +"Honduras","HND","HN|HND|340","HND",".hn","" +"Hong Kong","HKG","HK|HKG|344","HKG",".hk","" +"Howland Island","XHO","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Hungary","HUN","HU|HUN|348","HUN",".hu","" +"Iceland","ISL","IS|ISL|352","ISL",".is","" +"India","IND","IN|IND|356","IND",".in","" +"Indonesia","IDN","ID|IDN|360","IDN",".id","" +"Iran","IRN","IR|IRN|364","IRN",".ir","" +"Iraq","IRQ","IQ|IRQ|368","IRQ",".iq","" +"Ireland","IRL","IE|IRL|372","IRL",".ie","" +"Isle of Man","IMN","IM|IMN|833","UK",".im","" +"Israel","ISR","IL|ISR|376","ISR",".il","" +"Italy","ITA","IT|ITA|380","ITA",".it","" +"Jamaica","JAM","JM|JAM|388","JAM",".jm","" +"Jan Mayen","XJM","-","SJM","-","ISO includes with Svalbard" +"Japan","JPN","JP|JPN|392","JPN",".jp","" +"Jarvis Island","XJV","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Jersey","JEY","JE|JEY|832","UK",".je","" +"Johnston Atoll","XJA","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Jordan","JOR","JO|JOR|400","JOR",".jo","" +"Juan de Nova Island","XJN","-","-","-","administered as part of French Southern and Antarctic Lands; no ISO codes assigned" +"Kazakhstan","KAZ","KZ|KAZ|398","KAZ",".kz","" +"Kenya","KEN","KE|KEN|404","KEN",".ke","" +"Kingman Reef","XKR","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Kiribati","KIR","KI|KIR|296","KIR",".ki","" +"Korea, North","PRK","KP|PRK|408","PRK",".kp","" +"Korea, South","KOR","KR|KOR|410","KOR",".kr","" +"Kosovo","XKS","XK|XKS|","-","-","XK and XKS are ISO 3166 user assigned codes; ISO 3166 Maintenace Authority has not assigned codes" +"Kuwait","KWT","KW|KWT|414","KWT",".kw","" +"Kyrgyzstan","KGZ","KG|KGZ|417","KGZ",".kg","" +"Laos","LAO","LA|LAO|418","LAO",".la","" +"Latvia","LVA","LV|LVA|428","LVA",".lv","" +"Lebanon","LBN","LB|LBN|422","LBN",".lb","" +"Lesotho","LSO","LS|LSO|426","LSO",".ls","" +"Liberia","LBR","LR|LBR|430","LBR",".lr","" +"Libya","LBY","LY|LBY|434","LBY",".ly","" +"Liechtenstein","LIE","LI|LIE|438","LIE",".li","" +"Lithuania","LTU","LT|LTU|440","LTU",".lt","" +"Luxembourg","LUX","LU|LUX|442","LUX",".lu","" +"Macau","MAC","MO|MAC|446","MAC",".mo","" +"Madagascar","MDG","MG|MDG|450","MDG",".mg","" +"Malawi","MWI","MW|MWI|454","MWI",".mw","" +"Malaysia","MYS","MY|MYS|458","MYS",".my","" +"Maldives","MDV","MV|MDV|462","MDV",".mv","" +"Mali","MLI","ML|MLI|466","MLI",".ml","" +"Malta","MLT","MT|MLT|470","MLT",".mt","" +"Marshall Islands","MHL","MH|MHL|584","MHL",".mh","" +"Martinique","MTQ","MQ|MTQ|474","MTQ",".mq","" +"Mauritania","MRT","MR|MRT|478","MRT",".mr","" +"Mauritius","MUS","MU|MUS|480","MUS",".mu","" +"Mayotte","MYT","YT|MYT|175","FRA",".yt","" +"Mexico","MEX","MX|MEX|484","MEX",".mx","" +"Micronesia, Federated States of","FSM","FM|FSM|583","FSM",".fm","" +"Midway Islands","XMW","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Moldova","MDA","MD|MDA|498","MDA",".md","" +"Monaco","MCO","MC|MCO|492","MCO",".mc","" +"Mongolia","MNG","MN|MNG|496","MNG",".mn","" +"Montenegro","MNE","ME|MNE|499","MNE",".me","" +"Montserrat","MSR","MS|MSR|500","MSR",".ms","" +"Morocco","MAR","MA|MAR|504","MAR",".ma","" +"Mozambique","MOZ","MZ|MOZ|508","MOZ",".mz","" +"Myanmar","-","-","-","-","see Burma" +"Namibia","NAM","NA|NAM|516","NAM",".na","" +"Nauru","NRU","NR|NRU|520","NRU",".nr","" +"Navassa Island","XNV","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Nepal","NPL","NP|NPL|524","NPL",".np","" +"Netherlands","NLD","NL|NLD|528","NLD",".nl","" +"New Caledonia","NCL","NC|NCL|540","NCL",".nc","" +"New Zealand","NZL","NZ|NZL|554","NZL",".nz","" +"Nicaragua","NIC","NI|NIC|558","NIC",".ni","" +"Niger","NER","NE|NER|562","NER",".ne","" +"Nigeria","NGA","NG|NGA|566","NGA",".ng","" +"Niue","NIU","NU|NIU|570","NIU",".nu","" +"Norfolk Island","NFK","NF|NFK|574","NFK",".nf","" +"North Macedonia","MKD","MK|MKD|807","FYR",".mk","" +"Northern Mariana Islands","MNP","MP|MNP|580","MNP",".mp","" +"Norway","NOR","NO|NOR|578","NOR",".no","" +"Oman","OMN","OM|OMN|512","OMN",".om","" +"Pakistan","PAK","PK|PAK|586","PAK",".pk","" +"Palau","PLW","PW|PLW|585","PLW",".pw","" +"Palmyra Atoll","XPL","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Panama","PAN","PA|PAN|591","PAN",".pa","" +"Papua New Guinea","PNG","PG|PNG|598","PNG",".pg","" +"Paracel Islands","XPR","-","-","-","" +"Paraguay","PRY","PY|PRY|600","PRY",".py","" +"Peru","PER","PE|PER|604","PER",".pe","" +"Philippines","PHL","PH|PHL|608","PHL",".ph","" +"Pitcairn Islands","PCN","PN|PCN|612","PCN",".pn","" +"Poland","POL","PL|POL|616","POL",".pl","" +"Portugal","PRT","PT|PRT|620","PRT",".pt","" +"Puerto Rico","PRI","PR|PRI|630","PRI",".pr","" +"Qatar","QAT","QA|QAT|634","QAT",".qa","" +"Reunion","REU","RE|REU|638","REU",".re","" +"Romania","ROU","RO|ROU|642","ROU",".ro","" +"Russia","RUS","RU|RUS|643","RUS",".ru","" +"Rwanda","RWA","RW|RWA|646","RWA",".rw","" +"Saint Barthelemy","BLM","BL|BLM|652","-",".bl","ccTLD .fr and .gp may also be used" +"Saint Helena, Ascension, and Tristan da Cunha","SHN","SH|SHN|654","SHN",".sh","includes Saint Helena Island, Ascension Island, and the Tristan da Cunha archipelago" +"Saint Kitts and Nevis","KNA","KN|KNA|659","KNA",".kn","" +"Saint Lucia","LCA","LC|LCA|662","LCA",".lc","" +"Saint Martin","MAF","MF|MAF|663","-",".mf","ccTLD .fr and .gp may also be used" +"Saint Pierre and Miquelon","SPM","PM|SPM|666","SPM",".pm","" +"Saint Vincent and the Grenadines","VCT","VC|VCT|670","VCT",".vc","" +"Samoa","WSM","WS|WSM|882","WSM",".ws","" +"San Marino","SMR","SM|SMR|674","SMR",".sm","" +"Sao Tome and Principe","STP","ST|STP|678","STP",".st","" +"Saudi Arabia","SAU","SA|SAU|682","SAU",".sa","" +"Senegal","SEN","SN|SEN|686","SEN",".sn","" +"Serbia","SRB","RS|SRB|688","-",".rs","" +"Seychelles","SYC","SC|SYC|690","SYC",".sc","" +"Sierra Leone","SLE","SL|SLE|694","SLE",".sl","" +"Singapore","SGP","SG|SGP|702","SGP",".sg","" +"Sint Maarten","SXM","SX|SXM|534","-",".sx","" +"Slovakia","SVK","SK|SVK|703","SVK",".sk","" +"Slovenia","SVN","SI|SVN|705","SVN",".si","" +"Solomon Islands","SLB","SB|SLB|090","SLB",".sb","" +"Somalia","SOM","SO|SOM|706","SOM",".so","" +"South Africa","ZAF","ZA|ZAF|710","ZAF",".za","" +"South Georgia and the Islands","SGS","GS|SGS|239","SGS",".gs","" +"South Sudan","SSD","SS|SSD|728","-","-","IANA has designated .ss as the ccTLD for South Sudan, however it has not been activated in DNS root zone" +"Spain","ESP","ES|ESP|724","ESP",".es","" +"Spratly Islands","XSP","-","-","-","" +"Sri Lanka","LKA","LK|LKA|144","LKA",".lk","" +"Sudan","SDN","SD|SDN|729","SDN",".sd","" +"Suriname","SUR","SR|SUR|740","SUR",".sr","" +"Svalbard","XSV","SJ|SJM|744","SJM",".sj","ISO includes Jan Mayen" +"Sweden","SWE","SE|SWE|752","SWE",".se","" +"Switzerland","CHE","CH|CHE|756","CHE",".ch","" +"Syria","SYR","SY|SYR|760","SYR",".sy","" +"Taiwan","TWN","TW|TWN|158","TWN",".tw","" +"Tajikistan","TJK","TJ|TJK|762","TJK",".tj","" +"Tanzania","TZA","TZ|TZA|834","TZA",".tz","" +"Thailand","THA","TH|THA|764","THA",".th","" +"Timor-Leste","TLS","TL|TLS|626","TLS",".tl","" +"Togo","TGO","TG|TGO|768","TGO",".tg","" +"Tokelau","TKL","TK|TKL|772","TKL",".tk","" +"Tonga","TON","TO|TON|776","TON",".to","" +"Trinidad and Tobago","TTO","TT|TTO|780","TTO",".tt","" +"Tromelin Island","XTR","-","-","-","administered as part of French Southern and Antarctic Lands; no ISO codes assigned" +"Tunisia","TUN","TN|TUN|788","TUN",".tn","" +"Turkey (Turkiye)","TUR","TR|TUR|792","TUR",".tr","" +"Turkmenistan","TKM","TM|TKM|795","TKM",".tm","" +"Turks and Caicos Islands","TCA","TC|TCA|796","TCA",".tc","" +"Tuvalu","TUV","TV|TUV|798","TUV",".tv","" +"Uganda","UGA","UG|UGA|800","UGA",".ug","" +"Ukraine","UKR","UA|UKR|804","UKR",".ua","" +"United Arab Emirates","ARE","AE|ARE|784","ARE",".ae","" +"United Kingdom","GBR","GB|GBR|826","GBR",".uk","for its internal communications, the European Union recommends the use of the code UK in lieu of the ISO 3166-2 code of GB" +"United States","USA","US|USA|840","USA",".us","" +"United States Minor Outlying Islands","-","UM|UMI|581","-",".um","ISO includes Baker Island, Howland Island, Jarvis Island, Johnston Atoll, Kingman Reef, Midway Islands, Navassa Island, Palmyra Atoll, Wake Island" +"Uruguay","URY","UY|URY|858","URY",".uy","" +"Uzbekistan","UZB","UZ|UZB|860","UZB",".uz","" +"Vanuatu","VUT","VU|VUT|548","VUT",".vu","" +"Venezuela","VEN","VE|VEN|862","VEN",".ve","" +"Vietnam","VNM","VN|VNM|704","VNM",".vn","" +"Virgin Islands","VIR","VI|VIR|850","VIR",".vi","" +"Virgin Islands (UK)","-","-","-",".vg","see British Virgin Islands" +"Virgin Islands (US)","-","-","-",".vi","see Virgin Islands" +"Wake Island","XWK","-","UMI","-","ISO includes with the US Minor Outlying Islands" +"Wallis and Futuna","WLF","WF|WLF|876","WLF",".wf","" +"West Bank","XWB","PS|PSE|275","PSE",".ps","ISO identifies as Occupied Palestinian Territory" +"Western Sahara","WI","EH|ESH|732","ESH",".eh","" +"Western Samoa","-","-","-",".ws","see Samoa" +"World","-","-","-","-","the Factbook uses the W data code from DIAM 65-18 Geopolitical Data Elements and Related Features, Data Standard No. 3, December 1994, published by the Defense Intelligence Agency" +"Yemen","YEM","YE|YEM|887","YEM",".ye","" +"Zaire","-","-","-","-","see Democratic Republic of the Congo" +"Zambia","ZMB","ZM|ZMB|894","ZMB",".zm","" +"Zimbabwe","ZWE","ZW|ZWE|716","ZWE",".zw","" \ No newline at end of file diff --git a/scripts/other/iso-countries.ts b/scripts/other/iso-countries.ts new file mode 100644 index 0000000..f01f6d7 --- /dev/null +++ b/scripts/other/iso-countries.ts @@ -0,0 +1,69 @@ +import * as fs from 'node:fs'; + +const sanitizeValue = (value: string) => { + return value.replace(/^["']/g, '').replace(/["']$/g, ''); +}; + +const sanitizeHeader = (header: string) => { + return header + .toLowerCase() + .replace(/[^a-z0-9]/g, '_') + .replace(/_+/g, '_') + .replace(/^_/, '') + .replace(/_$/, ''); +}; + +function lineToArray(text) { + let p = '', + row = [''], + ret = [row], + i = 0, + r = 0, + s = !0, + l; + for (l of text) { + if ('"' === l) { + if (s && l === p) row[i] += l; + s = !s; + } else if (',' === l && s) l = row[++i] = ''; + else if ('\n' === l && s) { + if ('\r' === p) row[i] = row[i].slice(0, -1); + row = ret[++r] = [(l = '')]; + i = 0; + } else row[i] += l; + p = l; + } + return ret; +} + +const fileContent = fs.readFileSync('./Country Data Codes.csv', 'utf8'); +const lines = lineToArray(fileContent); +// @ts-expect-error - clearly checking for length above +const headers = lines[0].map(sanitizeHeader); +const rows = []; +for (const line of lines.slice(1)) { + const values = line.map(sanitizeValue); + + const rawRow = headers.reduce>((acc, header, i) => { + acc[header] = values[i] ?? ''; + return acc; + }, {}); + + if (rawRow['iso_3166'] === '-') { + continue; + } + const [alpha2, alpha3, numeric] = rawRow['iso_3166'].split('|'); + + if (alpha2 === 'COD') { + console.log(numeric, alpha2); + } + rows.push({ + name: rawRow['name'], + alpha: alpha2, + numeric: +numeric, + }); +} + +const dataJson = JSON.stringify(rows, null); +const ts = `export const COUNTRIES = ${dataJson};`; +fs.writeFileSync('../../src/lib/data/countries.ts', ts); diff --git a/scripts/other/update-airports-data.cjs b/scripts/other/update-airports-data.cjs index 60d2417..0d7c28c 100644 --- a/scripts/other/update-airports-data.cjs +++ b/scripts/other/update-airports-data.cjs @@ -1,7 +1,6 @@ var fs = require('fs'); const IGNORED_FIELDS = [ - 'GPS', 'LOCAL', 'type', 'restriction', @@ -43,7 +42,12 @@ const IGNORED_FIELDS = [ if (!isNaN(+cell) && cell !== null) { cell = +cell; } - airport[header] = cell; + + if (i === 2 && airport['ICAO'].length !== 4 && cell?.length === 4) { + airport['ICAO'] = cell; + } else { + airport[header] = cell; + } }); return airport; }) @@ -51,7 +55,7 @@ const IGNORED_FIELDS = [ const airportsDataJson = JSON.stringify(airportsData, null); const airportsTs = `export const AIRPORTS = ${airportsDataJson};`; - fs.writeFileSync('../src/lib/data/airports.ts', airportsTs); + fs.writeFileSync('../../src/lib/data/airports.ts', airportsTs); })(); const sanitizeValue = (value) => { diff --git a/src/app.css b/src/app.css index 6c205f5..0b8acc8 100644 --- a/src/app.css +++ b/src/app.css @@ -5,96 +5,129 @@ @tailwind utilities; @layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-hover: 0 0% 95%; - --card-foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-hover: 0 0% 95%; + --card-foreground: 240 10% 3.9%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; - --primary: 217 91% 60%; - --primary-foreground: 0 0% 98%; + --primary: 217 91% 60%; + --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; - --destructive: 0 72.2% 50.6%; - --destructive-foreground: 0 0% 98%; + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 0 0% 98%; - --ring: 240 10% 3.9%; + --ring: 240 10% 3.9%; - --radius: 0.5rem; - } + --radius: 0.5rem; + } - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-hover: 240 10% 12%; - --card-foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-hover: 240 10% 12%; + --card-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; - --primary: 217 91% 60%; - --primary-foreground: 240 5.9% 10%; + --primary: 217 91% 60%; + --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; - --destructive: 0 68% 45%; - --destructive-foreground: 0 0% 98%; + --destructive: 0 68% 45%; + --destructive-foreground: 0 0% 98%; - --ring: 240 4.9% 83.9%; - } + --ring: 240 4.9% 83.9%; + } } @layer base { - * { - @apply border-border; - } + * { + @apply border-border; + } - body { - @apply bg-background text-foreground; - } + body { + @apply bg-background text-foreground; + } } html, body { - @apply h-full; + @apply h-full; } .page-container { - @apply container mx-auto px-4 md:px-10 py-10 space-y-10; + @apply container mx-auto px-4 md:px-10 py-10 space-y-10; } .maplibregl-popup-content { - @apply !p-0 !bg-popover dark:!bg-dark-2 !text-popover-foreground !drop-shadow-2xl !rounded-lg; + @apply !p-0 !bg-popover dark:!bg-dark-2 !text-popover-foreground !drop-shadow-2xl !rounded-lg; } .maplibregl-popup-tip { - @apply !border-none; + @apply !border-none; +} + +/* Basic prose styles */ +.prose h1 { + @apply text-4xl leading-[1.11] font-bold; +} + +.prose h2 { + @apply text-2xl leading-[1.33] font-bold mt-[1em]; +} + +.prose h3 { + @apply text-xl leading-[1.6] font-semibold mb-2 mt-5; +} + +:where(.prose > :first-child) { + @apply !mt-0; +} + +.prose :where(h3 + *) { + @apply !mt-0; +} + +.prose ul { + @apply list-disc pl-6 mb-4; +} + +.prose a { + @apply font-medium border-b border-b-primary; +} + +.prose a:hover { + @apply border-b-2 } diff --git a/src/app.d.ts b/src/app.d.ts index 54d35d3..d3132a2 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,11 +1,16 @@ +import type { AppConfig } from '$lib/db/types'; + declare global { namespace App { interface Locals { user: import('lucia').User | null; session: import('lucia').Session | null; + appConfig: AppConfig | null; } + interface PageData { user: import('lucia').User | null; + users: Omit[]; } namespace Superforms { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index ba19521..8dbc132 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,11 +1,13 @@ import { lucia } from '$lib/server/auth'; import type { Cookie } from 'lucia'; +import { fetchAppConfig } from '$lib/server/utils/config'; export async function handle({ event, resolve }) { const sessionId = event.cookies.get(lucia.sessionCookieName); if (!sessionId) { event.locals.user = null; event.locals.session = null; + event.locals.appConfig = await fetchAppConfig(); return resolve(event); } @@ -23,7 +25,9 @@ export async function handle({ event, resolve }) { ...sessionCookie.attributes, }); } + event.locals.user = user; event.locals.session = session; + event.locals.appConfig = await fetchAppConfig(); return resolve(event); } diff --git a/src/lib/components/dock/DockDropdownItem.svelte b/src/lib/components/dock/DockDropdownItem.svelte new file mode 100644 index 0000000..66f5625 --- /dev/null +++ b/src/lib/components/dock/DockDropdownItem.svelte @@ -0,0 +1,115 @@ + + + e.key === 'Escape' && (open = false)} /> + +
e.key === 'Enter' && (open = !open)} + onclick={() => (open = !open)} + class="flex aspect-square cursor-pointer items-center justify-center rounded-full" +> + + + {@render children()} + + +

{label}

+
+
+
+ + +
+ diff --git a/src/lib/components/dock/DockFloatingItem.svelte b/src/lib/components/dock/DockFloatingItem.svelte new file mode 100644 index 0000000..918c48c --- /dev/null +++ b/src/lib/components/dock/DockFloatingItem.svelte @@ -0,0 +1,31 @@ + + +
+ +
+ + + {@render children()} + + +

{label}

+
+
+
+
+
diff --git a/src/lib/components/dock/DockTooltipItem.svelte b/src/lib/components/dock/DockTooltipItem.svelte index 3d72f78..0b5f5a6 100644 --- a/src/lib/components/dock/DockTooltipItem.svelte +++ b/src/lib/components/dock/DockTooltipItem.svelte @@ -28,7 +28,7 @@ - +

{item.label}

@@ -40,12 +40,7 @@ - +

{item.label}

diff --git a/src/lib/components/dock/index.ts b/src/lib/components/dock/index.ts index c0dafe2..d7d5d07 100644 --- a/src/lib/components/dock/index.ts +++ b/src/lib/components/dock/index.ts @@ -1,3 +1,5 @@ export { default as Dock } from './Dock.svelte'; export { default as DockItem } from './DockItem.svelte'; export { default as DockTooltipItem } from './DockTooltipItem.svelte'; +export { default as DockDropdownItem } from './DockDropdownItem.svelte'; +export { default as DockFloatingItem } from './DockFloatingItem.svelte'; diff --git a/src/lib/components/form-fields/AircraftField.svelte b/src/lib/components/form-fields/AircraftField.svelte index a28ebbf..0250ce8 100644 --- a/src/lib/components/form-fields/AircraftField.svelte +++ b/src/lib/components/form-fields/AircraftField.svelte @@ -59,7 +59,6 @@ { key: 'icao', exact: true }, { key: 'name', exact: false }, ], - 20, ); } else { aircraft = []; diff --git a/src/lib/components/form-fields/AirlineField.svelte b/src/lib/components/form-fields/AirlineField.svelte index b7ee9d8..b1373b0 100644 --- a/src/lib/components/form-fields/AirlineField.svelte +++ b/src/lib/components/form-fields/AirlineField.svelte @@ -38,6 +38,19 @@ $formData.airline = item?.value ?? null; }); + // If the field is updated externally, update the selected value + formData.subscribe((data) => { + if (data['airline'] === $selected?.value) return; + selected.set( + data['airline'] + ? { + label: airlineFromICAO(data['airline'])?.name, + value: data['airline'], + } + : undefined, + ); + }); + $effect(() => { if (!$open) { $inputValue = $selected?.label ?? ''; @@ -47,16 +60,11 @@ let airlines: Airline[] = $state([]); $effect(() => { if ($touchedInput && $inputValue !== '') { - airlines = sortAndFilterByMatch( - AIRLINES, - $inputValue, - [ - { key: 'icao', exact: true }, - { key: 'iata', exact: true }, - { key: 'name', exact: false }, - ], - 20, - ); + airlines = sortAndFilterByMatch(AIRLINES, $inputValue, [ + { key: 'icao', exact: true }, + { key: 'iata', exact: true }, + { key: 'name', exact: false }, + ]); } else { airlines = []; } diff --git a/src/lib/components/form-fields/AirportField.svelte b/src/lib/components/form-fields/AirportField.svelte index fc88f74..2531125 100644 --- a/src/lib/components/form-fields/AirportField.svelte +++ b/src/lib/components/form-fields/AirportField.svelte @@ -9,7 +9,11 @@ import { z } from 'zod'; import type { flightSchema } from '$lib/zod/flight'; import { writable } from 'svelte/store'; - import { type Airport, airportFromICAO, airportSearchCache } from '$lib/utils/data/airports'; + import { + type Airport, + airportFromICAO, + airportSearchCache, + } from '$lib/utils/data/airports'; let { field, @@ -42,6 +46,19 @@ } }); + // If the field is updated externally, update the selected value + formData.subscribe((data) => { + if (data[field] === $selected?.value) return; + selected.set( + data[field] + ? { + label: airportFromICAO(data[field])?.name, + value: data[field], + } + : undefined, + ); + }); + $effect(() => { if (!$open) { $inputValue = $selected?.label ?? ''; diff --git a/src/lib/components/map/Map.svelte b/src/lib/components/map/Map.svelte index 45988ea..fe462da 100644 --- a/src/lib/components/map/Map.svelte +++ b/src/lib/components/map/Map.svelte @@ -28,6 +28,7 @@ const FROM_COLOR = [59, 130, 246]; // Also the primary color const TO_COLOR = [139, 92, 246]; // TW violet-500 const HOVER_COLOR = [16, 185, 129]; + const FUTURE_COLOR = [102, 217, 239, 100]; let { flights, @@ -102,9 +103,17 @@ getSourcePosition={(d) => d.from.position} getTargetPosition={(d) => d.to.position} getSourceColor={(d) => - hoveredArc && d === hoveredArc ? HOVER_COLOR : FROM_COLOR} + hoveredArc && d === hoveredArc + ? HOVER_COLOR + : d.exclusivelyFuture + ? FUTURE_COLOR + : FROM_COLOR} getTargetColor={(d) => - hoveredArc && d === hoveredArc ? HOVER_COLOR : TO_COLOR} + hoveredArc && d === hoveredArc + ? HOVER_COLOR + : d.exclusivelyFuture + ? FUTURE_COLOR + : TO_COLOR} updateTriggers={{ getSourceColor: hoveredArc, getTargetColor: hoveredArc }} getWidth={2} getHeight={0} diff --git a/src/lib/components/modals/add-flight/AddFlightModal.svelte b/src/lib/components/modals/add-flight/AddFlightModal.svelte index 91daf22..111d263 100644 --- a/src/lib/components/modals/add-flight/AddFlightModal.svelte +++ b/src/lib/components/modals/add-flight/AddFlightModal.svelte @@ -9,11 +9,10 @@ import FlightInformation from './FlightInformation.svelte'; import SeatInformation from './SeatInformation.svelte'; import { page } from '$app/stores'; + import FlightNumber from '$lib/components/modals/add-flight/FlightNumber.svelte'; + import { trpc } from '$lib/trpc'; - let { - open = $bindable(), - invalidator, - }: { open: boolean; invalidator: { onSuccess: () => void } } = $props(); + let { open = $bindable() }: { open: boolean } = $props(); const form = superForm( defaults>(zod(flightSchema)), @@ -23,7 +22,7 @@ onUpdated({ form }) { if (form.message) { if (form.message.type === 'success') { - invalidator.onSuccess(); + trpc.flight.list.utils.invalidate(); open = false; return void toast.success(form.message.text); } @@ -48,7 +47,8 @@ classes="max-h-full overflow-y-auto max-w-lg" >

Add Flight

-
+ + diff --git a/src/lib/components/modals/add-flight/FlightInformation.svelte b/src/lib/components/modals/add-flight/FlightInformation.svelte index 59475d1..fde19e1 100644 --- a/src/lib/components/modals/add-flight/FlightInformation.svelte +++ b/src/lib/components/modals/add-flight/FlightInformation.svelte @@ -21,55 +21,9 @@

Flight Information

- - -
- - - Flight Reason - { - if (value) { - if (value.value === $formData.flightReason) { - $formData.flightReason = null; - } else { - $formData.flightReason = value.value; - } - } - }} - > - - - - - - - - - - - - - - - - - Flight Number - - - - - +
+ + Aircraft Registration @@ -77,6 +31,41 @@
+ + + + Flight Reason + { + if (value) { + if (value.value === $formData.flightReason) { + $formData.flightReason = null; + } else { + $formData.flightReason = value.value; + } + } + }} + > + + + + + + + + + + + + + + Notes diff --git a/src/lib/components/modals/add-flight/FlightNumber.svelte b/src/lib/components/modals/add-flight/FlightNumber.svelte new file mode 100644 index 0000000..6a01822 --- /dev/null +++ b/src/lib/components/modals/add-flight/FlightNumber.svelte @@ -0,0 +1,74 @@ + + + + + Flight Number +
+ + +
+
+
diff --git a/src/lib/components/modals/edit-flight/EditFlightModal.svelte b/src/lib/components/modals/edit-flight/EditFlightModal.svelte index 4185458..19c19ba 100644 --- a/src/lib/components/modals/edit-flight/EditFlightModal.svelte +++ b/src/lib/components/modals/edit-flight/EditFlightModal.svelte @@ -13,6 +13,7 @@ import SeatInformation from '$lib/components/modals/add-flight/SeatInformation.svelte'; import FlightInformation from '$lib/components/modals/add-flight/FlightInformation.svelte'; import { toISOString } from '$lib/utils/index.js'; + import FlightNumber from '$lib/components/modals/add-flight/FlightNumber.svelte'; const timeFormatter = new Intl.DateTimeFormat(undefined, { timeZone: 'UTC', @@ -75,7 +76,13 @@

Edit Flight

- + + diff --git a/src/lib/components/modals/index.ts b/src/lib/components/modals/index.ts index 50262b3..2e02e30 100644 --- a/src/lib/components/modals/index.ts +++ b/src/lib/components/modals/index.ts @@ -3,3 +3,6 @@ export { default as SettingsModal } from './settings/SettingsModal.svelte'; export { default as ListFlightsModal } from '$lib/components/modals/list-flights/ListFlightsModal.svelte'; export { default as StatisticsModal } from './statistics/StatisticsModal.svelte'; export { default as EditFlightModal } from './edit-flight/EditFlightModal.svelte'; +export { default as SetupVisitedCountries } from './visited-countries/SetupVisitedCountries.svelte'; +export { default as EditVisitedCountry } from './visited-countries/EditVisitedCountry.svelte'; +export { default as NewVersionAnnouncement } from './new-version-announcement/NewVersionAnnouncement.svelte'; diff --git a/src/lib/components/modals/new-version-announcement/NewTabLink.svelte b/src/lib/components/modals/new-version-announcement/NewTabLink.svelte new file mode 100644 index 0000000..47aac2f --- /dev/null +++ b/src/lib/components/modals/new-version-announcement/NewTabLink.svelte @@ -0,0 +1,19 @@ + + + + {#if prNum} + #{prNum} + {:else} + {@render children()} + {/if} + diff --git a/src/lib/components/modals/new-version-announcement/NewVersionAnnouncement.svelte b/src/lib/components/modals/new-version-announcement/NewVersionAnnouncement.svelte new file mode 100644 index 0000000..8c8124e --- /dev/null +++ b/src/lib/components/modals/new-version-announcement/NewVersionAnnouncement.svelte @@ -0,0 +1,71 @@ + + +{#if changelog} + + + + + New version available! + {changelog.name} + + +
+ +
+ + + Got it + +
+
+{/if} diff --git a/src/lib/components/modals/settings/SettingsModal.svelte b/src/lib/components/modals/settings/SettingsModal.svelte index 376f01f..0aa6f77 100644 --- a/src/lib/components/modals/settings/SettingsModal.svelte +++ b/src/lib/components/modals/settings/SettingsModal.svelte @@ -5,24 +5,39 @@ import { cubicInOut } from 'svelte/easing'; import { Modal } from '$lib/components/ui/modal'; import { Button } from '$lib/components/ui/button'; - import { ImportPage, GeneralPage, AppearancePage, UsersPage } from './pages'; + import { + ImportPage, + GeneralPage, + AppearancePage, + UsersPage, + ExportPage, + OAuthPage, + SecurityPage, + } from './pages'; import { page } from '$app/stores'; + import { isLargeScreen } from '$lib/utils/size'; + import SettingsTabContainer from '$lib/components/modals/settings/SettingsTabContainer.svelte'; - const SETTINGS_PAGES = [ + const ACCOUNT_SETTINGS = [ { title: 'General', id: 'general' }, - { title: 'Users', id: 'users' }, + { title: 'Security', id: 'security' }, { title: 'Appearance', id: 'appearance' }, { title: 'Import', id: 'import' }, + { title: 'Export', id: 'export' }, ] as const; + const ADMIN_SETTINGS = [ + { title: 'Users', id: 'users' }, + { title: 'OAuth', id: 'oauth' }, + ]; let { open = $bindable(), - invalidator, activeTab = 'general', }: { open: boolean; - invalidator?: { onSuccess: () => void }; - activeTab?: (typeof SETTINGS_PAGES)[number]['id']; + activeTab?: + | (typeof ACCOUNT_SETTINGS)[number]['id'] + | (typeof ADMIN_SETTINGS)[number]['id']; } = $props(); const user = $derived($page.data.user); @@ -31,11 +46,6 @@ duration: 250, easing: cubicInOut, }); - - const authorized_pages = SETTINGS_PAGES.filter((value) => { - if (user?.role !== 'user') return true; - return value.id !== 'users'; - }); @@ -52,13 +62,13 @@
-
{#if activeTab === 'general'} - {:else if activeTab === 'users'} - + {:else if activeTab === 'security'} + {:else if activeTab === 'appearance'} {:else if activeTab === 'import'} - + + {:else if activeTab === 'export'} + + {:else if activeTab === 'users'} + + {:else if activeTab === 'oauth'} + {/if}
diff --git a/src/lib/components/modals/settings/SettingsTabContainer.svelte b/src/lib/components/modals/settings/SettingsTabContainer.svelte new file mode 100644 index 0000000..cf49967 --- /dev/null +++ b/src/lib/components/modals/settings/SettingsTabContainer.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/lib/components/modals/settings/pages/ExportPage.svelte b/src/lib/components/modals/settings/pages/ExportPage.svelte new file mode 100644 index 0000000..b46513a --- /dev/null +++ b/src/lib/components/modals/settings/pages/ExportPage.svelte @@ -0,0 +1,64 @@ + + + + {#snippet subtitleHtml()} +

+ Export your data. Learn more about the data formats in the documentation. +

+ {/snippet} +
+ + +
+
diff --git a/src/lib/components/modals/settings/pages/ImportPage.svelte b/src/lib/components/modals/settings/pages/ImportPage.svelte index 77ca6e1..653f656 100644 --- a/src/lib/components/modals/settings/pages/ImportPage.svelte +++ b/src/lib/components/modals/settings/pages/ImportPage.svelte @@ -8,12 +8,6 @@ import { processFile } from '$lib/import'; import { toast } from 'svelte-sonner'; - let { - invalidator, - }: { - invalidator?: { onSuccess: () => void }; - } = $props(); - let files: FileList | null = $state(null); let fileError: string | null = $state(null); @@ -21,7 +15,11 @@ const file = files?.[0]; if (!file) return; - if (!file.name.endsWith('.csv') && !file.name.endsWith('.txt')) { + if ( + !file.name.endsWith('.csv') && + !file.name.endsWith('.txt') && + !file.name.endsWith('.json') + ) { fileError = 'File type not supported'; } else if (file.size > 5 * 1024 * 1024) { fileError = 'File must be less than 5MB'; @@ -31,6 +29,11 @@ }; const canImport = $derived(!!files?.[0] && !fileError); + const invalidator = { + onSuccess: () => { + trpc.flight.list.utils.invalidate(); + }, + }; const createMany = trpc.flight.createMany.mutation(invalidator); const handleImport = async () => { @@ -53,7 +56,7 @@
diff --git a/src/lib/components/modals/statistics/charts/PieChart.svelte b/src/lib/components/modals/statistics/charts/PieChart.svelte index d0eae51..d61fe58 100644 --- a/src/lib/components/modals/statistics/charts/PieChart.svelte +++ b/src/lib/components/modals/statistics/charts/PieChart.svelte @@ -23,18 +23,20 @@ value, }))} x="value" - r="label" - rScale={scaleOrdinal()} - rDomain={Object.entries(data) + c="label" + cScale={scaleOrdinal()} + cDomain={Object.entries(data) .sort((a, b) => b[1] - a[1]) .map(([key]) => key)} - rRange={['#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef']} + cRange={['#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef']} let:tooltip > - + - d.label} /> + + {data.label} +
diff --git a/src/lib/components/modals/visited-countries/EditVisitedCountry.svelte b/src/lib/components/modals/visited-countries/EditVisitedCountry.svelte new file mode 100644 index 0000000..4ddc6e8 --- /dev/null +++ b/src/lib/components/modals/visited-countries/EditVisitedCountry.svelte @@ -0,0 +1,106 @@ + + + +

{countryData?.name}

+
+ {@render statusRadioItem('visited', 'Visited')} + {@render statusRadioItem('lived', 'Lived')} + {@render statusRadioItem('wishlist', 'Wishlist')} + {@render statusRadioItem('layover', 'Layover')} +
+