├── front ├── src │ ├── routes │ │ ├── +layout.ts │ │ ├── +page.ts │ │ ├── login │ │ │ └── +page.ts │ │ ├── register │ │ │ └── +page.ts │ │ ├── sealife │ │ │ ├── new │ │ │ │ ├── +page.ts │ │ │ │ └── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── edit │ │ │ │ └── [id] │ │ │ │ │ └── +page.ts │ │ │ └── [slug] │ │ │ │ └── +page.ts │ │ ├── divesites │ │ │ ├── +page.ts │ │ │ ├── map │ │ │ │ └── [[region]] │ │ │ │ │ └── +page.ts │ │ │ ├── edit │ │ │ │ └── [id] │ │ │ │ │ └── +page.ts │ │ │ ├── new │ │ │ │ └── +page.svelte │ │ │ └── +page.svelte │ │ ├── dives │ │ │ ├── +page.ts │ │ │ ├── edit │ │ │ │ └── [id] │ │ │ │ │ └── +page.ts │ │ │ ├── plan │ │ │ │ └── +page.svelte │ │ │ ├── new │ │ │ │ └── +page.svelte │ │ │ ├── [id] │ │ │ │ └── +page.ts │ │ │ └── +page.svelte │ │ ├── photos │ │ │ ├── +page.ts │ │ │ ├── edit │ │ │ │ └── [id] │ │ │ │ │ └── +page.ts │ │ │ ├── +page.svelte │ │ │ └── [id] │ │ │ │ └── +page.ts │ │ ├── users │ │ │ └── [username] │ │ │ │ ├── +page.ts │ │ │ │ ├── dives │ │ │ │ ├── +page.ts │ │ │ │ └── +page.svelte │ │ │ │ └── photos │ │ │ │ ├── +page.ts │ │ │ │ └── +page.svelte │ │ ├── admin │ │ │ ├── +page.ts │ │ │ └── region │ │ │ │ ├── edit │ │ │ │ └── [id] │ │ │ │ │ └── +page.ts │ │ │ │ └── new │ │ │ │ └── +page.svelte │ │ ├── +error.svelte │ │ ├── sites │ │ │ └── [slug] │ │ │ │ └── +page.ts │ │ ├── facebook │ │ │ └── login │ │ │ │ └── +page.svelte │ │ ├── verify-email │ │ │ └── +page.svelte │ │ └── feedback │ │ │ └── +page.svelte │ ├── lib │ │ ├── graphql │ │ │ ├── queries │ │ │ │ ├── fbAppId.gql │ │ │ │ ├── regions.gql │ │ │ │ ├── currentUser.gql │ │ │ │ ├── feedback.gql │ │ │ │ ├── getDive.gql │ │ │ │ ├── getUser.gql │ │ │ │ ├── search.gql │ │ │ │ ├── frontpage.gql │ │ │ │ ├── getDives.gql │ │ │ │ ├── categories.gql │ │ │ │ ├── getDiveSites.gql │ │ │ │ ├── getPhotos.gql │ │ │ │ └── getSealife.gql │ │ │ ├── mutations │ │ │ │ ├── removeDive.gql │ │ │ │ ├── removePhoto.gql │ │ │ │ ├── resentVerification.gql │ │ │ │ ├── removeDiveSite.gql │ │ │ │ ├── removeSealife.gql │ │ │ │ ├── removeReference.gql │ │ │ │ ├── deleteUser.gql │ │ │ │ ├── addDive.gql │ │ │ │ ├── addDiveSite.gql │ │ │ │ ├── addFeedback.gql │ │ │ │ ├── planDive.gql │ │ │ │ ├── mergeDiveSite.gql │ │ │ │ ├── updatePhoto.gql │ │ │ │ ├── addSealife.gql │ │ │ │ ├── syncSubsurface.gql │ │ │ │ ├── verifyEmail.gql │ │ │ │ ├── fbLoginUser.gql │ │ │ │ ├── registerUser.gql │ │ │ │ ├── addReference.gql │ │ │ │ ├── regions.gql │ │ │ │ ├── fbRegisterUser.gql │ │ │ │ ├── comments.gql │ │ │ │ ├── likes.gql │ │ │ │ ├── passwordReset.gql │ │ │ │ └── loginUser.gql │ │ │ ├── fragments │ │ │ │ ├── reference.gql │ │ │ │ ├── region.gql │ │ │ │ ├── comments.gql │ │ │ │ ├── searchResult.gql │ │ │ │ ├── feedback.gql │ │ │ │ ├── categories.gql │ │ │ │ ├── photo.gql │ │ │ │ ├── diveSchedule.gql │ │ │ │ ├── sealife.gql │ │ │ │ ├── user.gql │ │ │ │ ├── diveSite.gql │ │ │ │ └── dive.gql │ │ │ └── client.ts │ │ ├── category.ts │ │ ├── util │ │ │ ├── formatMinutes.ts │ │ │ ├── formatGas.ts │ │ │ ├── fbRedirect.ts │ │ │ ├── imageAlt.ts │ │ │ └── bounds.ts │ │ ├── components │ │ │ ├── dives │ │ │ │ ├── GraphPlaceholder.svelte │ │ │ │ ├── GraphImage.svelte │ │ │ │ ├── DiveComment.svelte │ │ │ │ ├── DiveGraph.svelte │ │ │ │ ├── DiveLabels.svelte │ │ │ │ ├── DiveList.svelte │ │ │ │ └── DiveSummary.svelte │ │ │ ├── CheckLogin.svelte │ │ │ ├── labels │ │ │ │ └── UserLabel.svelte │ │ │ ├── FormRow.svelte │ │ │ ├── leaflet │ │ │ │ ├── Icon.svelte │ │ │ │ ├── EventBridge.ts │ │ │ │ ├── Popup.svelte │ │ │ │ ├── TileLayer.svelte │ │ │ │ ├── LeafletMap.svelte │ │ │ │ └── DrawRectangle.svelte │ │ │ ├── categories │ │ │ │ └── CategoryView.svelte │ │ │ ├── photos │ │ │ │ └── PhotoSlide.svelte │ │ │ ├── SiteMetrics.svelte │ │ │ ├── EditableMap.svelte │ │ │ ├── DiveSiteSummary.svelte │ │ │ ├── SealifeSummary.svelte │ │ │ ├── EditableRegion.svelte │ │ │ ├── SearchResult.svelte │ │ │ └── forms │ │ │ │ ├── LikeHeart.svelte │ │ │ │ ├── EditPhoto.svelte │ │ │ │ ├── Markdown.svelte │ │ │ │ └── VerifyEmail.svelte │ │ ├── icons │ │ │ ├── DiveSiteIcon.svelte │ │ │ ├── BaseIcon.svelte │ │ │ ├── CheckIcon.svelte │ │ │ ├── HeartFilled.svelte │ │ │ ├── SearchIcon.svelte │ │ │ ├── ChatFilled.svelte │ │ │ ├── DiveLogIcon.svelte │ │ │ ├── LinkIcon.svelte │ │ │ ├── ChatOutline.svelte │ │ │ ├── PhotoIcon.svelte │ │ │ ├── HeartOutline.svelte │ │ │ └── MastodonIcon.svelte │ │ └── session.ts │ ├── fonts │ │ └── Asap-VariableFont_wght.ttf │ ├── style │ │ └── spectre │ │ │ ├── functions │ │ │ ├── index.scss │ │ │ ├── _strip-unit.scss │ │ │ ├── _var-negative.scss │ │ │ ├── _color.scss │ │ │ └── _get-var.scss │ │ │ ├── mixins │ │ │ ├── _text.scss │ │ │ ├── _clearfix.scss │ │ │ ├── index.scss │ │ │ ├── _avatar.scss │ │ │ ├── _set-var.scss │ │ │ ├── _shadow.scss │ │ │ ├── _define-color.scss │ │ │ ├── _toast.scss │ │ │ ├── _define-color-based-on.scss │ │ │ ├── _box-shadow-side.scss │ │ │ ├── _color.scss │ │ │ └── _label.scss │ │ │ ├── _icons.scss │ │ │ ├── css-variables │ │ │ ├── _color-scheme.scss │ │ │ ├── _transition-duration.scss │ │ │ ├── _other-colors.scss │ │ │ ├── _border-width.scss │ │ │ ├── _z-index.scss │ │ │ ├── _body-colors.scss │ │ │ ├── _control-size.scss │ │ │ ├── _bg-colors.scss │ │ │ ├── _control-colors.scss │ │ │ ├── _layout-spacing.scss │ │ │ ├── _link-colors.scss │ │ │ ├── _control-width.scss │ │ │ ├── index.scss │ │ │ ├── _gray-colors.scss │ │ │ ├── _font-size.scss │ │ │ ├── _border-colors.scss │ │ │ ├── _parallax.scss │ │ │ ├── _responsive-breakpoints.scss │ │ │ ├── _unit-sizes.scss │ │ │ ├── _core-colors.scss │ │ │ └── _control-padding.scss │ │ │ ├── _utilities.scss │ │ │ ├── utilities │ │ │ ├── _shapes.scss │ │ │ ├── _cursors.scss │ │ │ ├── _display.scss │ │ │ ├── _text.scss │ │ │ ├── _position.scss │ │ │ └── _loading.scss │ │ │ ├── spectre-icons.scss │ │ │ ├── _mixins.scss │ │ │ ├── _animations.scss │ │ │ ├── _hero.scss │ │ │ ├── spectre-exp.scss │ │ │ ├── _navbar.scss │ │ │ ├── _panels.scss │ │ │ ├── _asian.scss │ │ │ ├── _viewer-360.scss │ │ │ ├── _tiles.scss │ │ │ ├── _dropdowns.scss │ │ │ ├── icons │ │ │ └── _icons-core.scss │ │ │ ├── _empty.scss │ │ │ ├── _filters.scss │ │ │ ├── _breadcrumbs.scss │ │ │ ├── _accordions.scss │ │ │ ├── spectre.scss │ │ │ ├── _navs.scss │ │ │ ├── _codes.scss │ │ │ ├── _autocomplete.scss │ │ │ ├── _base.scss │ │ │ ├── _chips.scss │ │ │ ├── _tables.scss │ │ │ ├── _badges.scss │ │ │ ├── _media.scss │ │ │ ├── _pagination.scss │ │ │ ├── _popovers.scss │ │ │ ├── _columns-order.scss │ │ │ ├── _cards.scss │ │ │ └── _progress.scss │ └── app.html ├── .npmrc ├── .graphqlrc.yml ├── static │ ├── logo.png │ ├── favicon.ico │ └── leaflet │ │ ├── marker-shadow.png │ │ ├── marker-icon-2x-blue.png │ │ ├── marker-icon-2x-grey.png │ │ └── marker-icon-2x-red.png ├── .prettierignore ├── .prettierrc ├── .gitignore ├── vite.config.ts ├── codegen.yml ├── Dockerfile ├── tsconfig.json ├── svelte.config.js ├── README.md └── package.json ├── .dockerignore ├── watermark.png ├── src ├── db │ ├── migrations │ │ ├── V007__user_name.sql │ │ ├── V003__add_date_field.sql │ │ ├── V010__photo_upload_date.sql │ │ ├── V018__regions_slug.sql │ │ ├── V022__deco_model.sql │ │ ├── V005__photo_site.sql │ │ ├── V009__fix_categories.sql │ │ ├── V021__email_verification.sql │ │ ├── V012__feedback_table.sql │ │ ├── V017__photo_fields.sql │ │ ├── V016__passwords.sql │ │ ├── V014__sealife_hide_location.sql │ │ ├── V011__sealife_revisions.sql │ │ ├── V013__watermark_features.sql │ │ ├── V002__regions_dive_site_photo.sql │ │ ├── V015__open_graph_references.sql │ │ ├── external_sql.rs │ │ ├── V020__external_users.sql │ │ ├── V004__slugify_sites.sql │ │ ├── V006__sealife.sql │ │ └── create_apub_keys.rs │ ├── feedback.rs │ ├── password_reset.rs │ └── email_verification.rs ├── static │ └── Asap-Bold.otf ├── schema │ ├── password_reset.rs │ ├── email_verification.rs │ ├── region.rs │ ├── feedback.rs │ └── categories.rs ├── schema.rs └── escape.rs ├── .gitignore ├── divedb_macro ├── Cargo.toml └── src │ └── lib.rs ├── templates ├── password_reset.txt ├── email_verification.txt ├── password_reset.mjml └── email_verification.mjml ├── divedb_core ├── Cargo.toml └── src │ └── lib.rs ├── Dockerfile └── docker-compose.yml /front/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Dockerfile 3 | docker-compose.yml 4 | -------------------------------------------------------------------------------- /watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/watermark.png -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/fbAppId.gql: -------------------------------------------------------------------------------- 1 | query fbAppId { 2 | fbAppId 3 | } 4 | -------------------------------------------------------------------------------- /src/db/migrations/V007__user_name.sql: -------------------------------------------------------------------------------- 1 | alter table users add column username text; -------------------------------------------------------------------------------- /front/.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: ./../schema.graphql 2 | documents: './src/**/*.gql' 3 | -------------------------------------------------------------------------------- /front/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/static/logo.png -------------------------------------------------------------------------------- /front/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/static/favicon.ico -------------------------------------------------------------------------------- /src/static/Asap-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/src/static/Asap-Bold.otf -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/regions.gql: -------------------------------------------------------------------------------- 1 | query getRegions { 2 | regions { 3 | ...RegionNode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/removeDive.gql: -------------------------------------------------------------------------------- 1 | mutation removeDive($id: UUID!) { 2 | removeDive(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/removePhoto.gql: -------------------------------------------------------------------------------- 1 | mutation removePhoto($id: UUID!) { 2 | removePhoto(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/resentVerification.gql: -------------------------------------------------------------------------------- 1 | mutation resendVerification { 2 | resendVerification 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/removeDiveSite.gql: -------------------------------------------------------------------------------- 1 | mutation removeDiveSite($id: UUID!) { 2 | removeDiveSite(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/removeSealife.gql: -------------------------------------------------------------------------------- 1 | mutation removeSealife($id: UUID!) { 2 | removeSealife(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/currentUser.gql: -------------------------------------------------------------------------------- 1 | query getCurrentUser { 2 | currentUser { 3 | ...CurrentUser 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/static/leaflet/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/static/leaflet/marker-shadow.png -------------------------------------------------------------------------------- /front/src/fonts/Asap-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/src/fonts/Asap-VariableFont_wght.ttf -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/removeReference.gql: -------------------------------------------------------------------------------- 1 | mutation removeReference($id: UUID!) { 2 | removeReference(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/deleteUser.gql: -------------------------------------------------------------------------------- 1 | mutation deleteUser($password: String!) { 2 | deleteUser(password: $password) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/feedback.gql: -------------------------------------------------------------------------------- 1 | query getFeedback($id: UUID) { 2 | feedback(id: $id) { 3 | ...FeedbackNode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/static/leaflet/marker-icon-2x-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/static/leaflet/marker-icon-2x-blue.png -------------------------------------------------------------------------------- /front/static/leaflet/marker-icon-2x-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/static/leaflet/marker-icon-2x-grey.png -------------------------------------------------------------------------------- /front/static/leaflet/marker-icon-2x-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cetra3/divedb/HEAD/front/static/leaflet/marker-icon-2x-red.png -------------------------------------------------------------------------------- /src/db/migrations/V003__add_date_field.sql: -------------------------------------------------------------------------------- 1 | 2 | alter table dive_sites add column "date" timestamp with time zone not null default now(); -------------------------------------------------------------------------------- /src/db/migrations/V010__photo_upload_date.sql: -------------------------------------------------------------------------------- 1 | alter table photos add column upload_date timestamp with time zone not null default now(); -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/getDive.gql: -------------------------------------------------------------------------------- 1 | query getDive($id: UUID!) { 2 | dives(id: $id) { 3 | ...DiveNode 4 | } 5 | siteUrl 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /cache 3 | .env 4 | db_dump.gz 5 | store/ 6 | thumbs/ 7 | large/ 8 | *.jpg 9 | deploy.sh 10 | chart/ 11 | Justfile -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/addDive.gql: -------------------------------------------------------------------------------- 1 | mutation addDive($dive: CreateDive!) { 2 | newDive(dive: $dive) { 3 | ...DiveNode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/style/spectre/functions/index.scss: -------------------------------------------------------------------------------- 1 | @forward 'color'; 2 | @forward 'get-var'; 3 | @forward 'strip-unit'; 4 | @forward 'var-negative'; 5 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/addDiveSite.gql: -------------------------------------------------------------------------------- 1 | mutation addDiveSite($site: CreateDiveSite!) { 2 | newDiveSite(site: $site) { 3 | ...Site 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/addFeedback.gql: -------------------------------------------------------------------------------- 1 | mutation addFeedback($feedback: String!) { 2 | addFeedback(feedback: $feedback) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/getUser.gql: -------------------------------------------------------------------------------- 1 | query getUser($username: String!) { 2 | user(username: $username) { 3 | ...UserInfo 4 | } 5 | siteUrl 6 | } 7 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/planDive.gql: -------------------------------------------------------------------------------- 1 | mutation planDive($plan: DivePlanInput!) { 2 | planDive(plan: $plan) { 3 | ...DiveScheduleNode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/reference.gql: -------------------------------------------------------------------------------- 1 | fragment Reference on OgReference { 2 | id 3 | url 4 | title 5 | description 6 | imageUrl 7 | lastFetched 8 | } 9 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/mergeDiveSite.gql: -------------------------------------------------------------------------------- 1 | mutation mergeDiveSites($fromId: UUID!, $toId: UUID!) { 2 | mergeDiveSites(fromId: $fromId, toId: $toId) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/updatePhoto.gql: -------------------------------------------------------------------------------- 1 | mutation updatePhoto($photo: CreatePhoto!) { 2 | updatePhoto(photo: $photo) { 3 | ...PhotoSummary 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/region.gql: -------------------------------------------------------------------------------- 1 | fragment RegionNode on Region { 2 | id 3 | name 4 | latMin 5 | lonMin 6 | latMax 7 | lonMax 8 | slug 9 | } 10 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/addSealife.gql: -------------------------------------------------------------------------------- 1 | mutation addSealife($sealife: CreateSealife!) { 2 | newSealife(sealife: $sealife) { 3 | ...SealifeNode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | bun.lock 6 | bun.lockb 7 | 8 | # Miscellaneous 9 | /static/ 10 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/search.gql: -------------------------------------------------------------------------------- 1 | query search($query: String!, $offset: Int) { 2 | search(query: $query, offset: $offset) { 3 | ...SearchResultNode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_text.scss: -------------------------------------------------------------------------------- 1 | // Text Ellipsis 2 | @mixin text-ellipsis() { 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/syncSubsurface.gql: -------------------------------------------------------------------------------- 1 | mutation syncSubsurface($email: String!, $password: String!) { 2 | syncSubsurface(email: $email, password: $password) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/frontpage.gql: -------------------------------------------------------------------------------- 1 | query frontPage { 2 | popularDiveSites { 3 | ...SiteSummaryMetrics 4 | } 5 | recentDives { 6 | ...DiveWithMetrics 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_clearfix.scss: -------------------------------------------------------------------------------- 1 | // Clearfix mixin 2 | @mixin clearfix() { 3 | &::after { 4 | clear: both; 5 | content: ''; 6 | display: table; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/comments.gql: -------------------------------------------------------------------------------- 1 | fragment Comment on DiveComment { 2 | id 3 | diveId 4 | user { 5 | ...UserSummary 6 | } 7 | date 8 | description 9 | } 10 | -------------------------------------------------------------------------------- /src/db/migrations/V018__regions_slug.sql: -------------------------------------------------------------------------------- 1 | alter table regions add column slug text; 2 | 3 | update regions set slug = slugify(name); 4 | 5 | alter table regions alter column slug set not null; -------------------------------------------------------------------------------- /src/db/migrations/V022__deco_model.sql: -------------------------------------------------------------------------------- 1 | alter table dives add column deco_model text; 2 | 3 | alter table dive_metrics add column o2 real; 4 | alter table dive_metrics add column he real; 5 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/searchResult.gql: -------------------------------------------------------------------------------- 1 | fragment SearchResultNode on SearchResult { 2 | id 3 | kind 4 | slug 5 | photoId 6 | name 7 | scientificName 8 | summary 9 | } 10 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/verifyEmail.gql: -------------------------------------------------------------------------------- 1 | mutation verifyEmail($email: String!, $token: UUID!) { 2 | verifyEmail(email: $email, token: $token) { 3 | ...CurrentUserToken 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/style/spectre/_icons.scss: -------------------------------------------------------------------------------- 1 | // CSS Icons 2 | @forward 'icons/icons-core'; 3 | @forward 'icons/icons-navigation'; 4 | @forward 'icons/icons-action'; 5 | @forward 'icons/icons-object'; 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/feedback.gql: -------------------------------------------------------------------------------- 1 | fragment FeedbackNode on Feedback { 2 | id 3 | user { 4 | id 5 | email 6 | level 7 | username 8 | } 9 | date 10 | feedback 11 | } 12 | -------------------------------------------------------------------------------- /front/src/lib/category.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export type CategoryMap = { [category: string]: string[] }; 4 | 5 | export const categoryStore = writable({}); 6 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/fbLoginUser.gql: -------------------------------------------------------------------------------- 1 | mutation fbLoginUser($redirectUri: String!, $code: String!) { 2 | fbLogin(redirectUri: $redirectUri, code: $code) { 3 | ...CurrentUserToken 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_color-scheme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Color scheme in :root and :host. 3 | */ 4 | :root, 5 | :host { 6 | // Scheme. 7 | color-scheme: normal; // light, dark, light dark 8 | } 9 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/registerUser.gql: -------------------------------------------------------------------------------- 1 | mutation registerUser($username: String!, $email: String!, $password: String!) { 2 | registerUser(username: $username, email: $email, password: $password) 3 | } 4 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/getDives.gql: -------------------------------------------------------------------------------- 1 | query getDives($diveSite: UUID, $username: String, $offset: Int) { 2 | dives(diveSite: $diveSite, username: $username, offset: $offset) { 3 | ...DiveWithMetrics 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | 3 | export async function load() { 4 | let frontPage = await getClient.frontPage(); 5 | 6 | return { 7 | frontPage 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/categories.gql: -------------------------------------------------------------------------------- 1 | query getCategories { 2 | categories { 3 | ...CategoryNode 4 | } 5 | } 6 | 7 | query getCategoryValues { 8 | categoryValues { 9 | ...CategoryValueNode 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /front/src/routes/login/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | 3 | export async function load() { 4 | let result = await client.fbAppId(); 5 | 6 | return { 7 | fbAppId: result.fbAppId 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/addReference.gql: -------------------------------------------------------------------------------- 1 | mutation newReference($url: String!, $sealifeId: UUID, $diveSiteId: UUID) { 2 | newReference(url: $url, sealifeId: $sealifeId, diveSiteId: $diveSiteId) { 3 | ...Reference 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/routes/register/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | 3 | export async function load() { 4 | let result = await client.fbAppId(); 5 | 6 | return { 7 | fbAppId: result.fbAppId 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /front/src/style/spectre/functions/_strip-unit.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | /* 4 | The function strips the unit from a given `$number`. 5 | */ 6 | @function strip-unit($number) { 7 | @return math.div($number, ($number * 0 + 1)); 8 | } 9 | -------------------------------------------------------------------------------- /src/db/migrations/V005__photo_site.sql: -------------------------------------------------------------------------------- 1 | 2 | alter table photos add column dive_site_id uuid REFERENCES dive_sites(id) ON DELETE SET NULL; 3 | 4 | update photos set dive_site_id = dives.dive_site_id from dives where dives.id = photos.dive_id; -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/regions.gql: -------------------------------------------------------------------------------- 1 | mutation newRegion($region: CreateRegion!) { 2 | newRegion(region: $region) { 3 | ...RegionNode 4 | } 5 | } 6 | 7 | mutation removeRegion($id: UUID!) { 8 | removeRegion(id: $id) 9 | } 10 | -------------------------------------------------------------------------------- /src/db/migrations/V009__fix_categories.sql: -------------------------------------------------------------------------------- 1 | 2 | alter table sealife_category_values drop column category_value_id; 3 | alter table sealife_category_values add column category_value_id uuid not null REFERENCES category_values(id) ON DELETE CASCADE -------------------------------------------------------------------------------- /front/src/routes/sealife/new/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | 3 | export async function load() { 4 | let { categories } = await getClient.getCategories(); 5 | 6 | return { 7 | categories 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /front/src/routes/divesites/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | 3 | export async function load() { 4 | let diveSites = await getClient.getDiveSitesSummaryMetrics(); 5 | 6 | return { 7 | diveSites 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/fbRegisterUser.gql: -------------------------------------------------------------------------------- 1 | mutation fbRegisterUser($username: String!, $redirectUri: String!, $code: String!) { 2 | fbRegisterUser(username: $username, redirectUri: $redirectUri, code: $code) { 3 | ...CurrentUserToken 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front/src/lib/util/formatMinutes.ts: -------------------------------------------------------------------------------- 1 | export default (time: number) => { 2 | const minutes = Math.floor(time / 60); 3 | 4 | const seconds = time - minutes * 60; 5 | 6 | return `${(minutes + '').padStart(2, '0')}:${(seconds + '').padStart(2, '0')}`; 7 | }; 8 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/categories.gql: -------------------------------------------------------------------------------- 1 | fragment CategoryNode on Category { 2 | id 3 | name 4 | values { 5 | ...CategoryValueNode 6 | } 7 | } 8 | 9 | fragment CategoryValueNode on CategoryValue { 10 | id 11 | categoryId 12 | value 13 | } 14 | -------------------------------------------------------------------------------- /src/db/migrations/V021__email_verification.sql: -------------------------------------------------------------------------------- 1 | create table if not exists email_verification ( 2 | id uuid primary key, 3 | user_id uuid not null REFERENCES users(id) ON DELETE CASCADE, 4 | "date" timestamp with time zone not null default now() 5 | ); 6 | -------------------------------------------------------------------------------- /src/db/migrations/V012__feedback_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists feedback ( 2 | id uuid primary key, 3 | "date" timestamp with time zone not null default now(), 4 | user_id uuid REFERENCES users(id) ON DELETE SET NULL, 5 | feedback text not null 6 | ); -------------------------------------------------------------------------------- /divedb_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "divedb_macro" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "AGPL-3.0" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | quote = "1" 12 | syn = { version = "2", features = ["full", "parsing"] } -------------------------------------------------------------------------------- /front/src/style/spectre/functions/_var-negative.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | 3 | /* 4 | The function `var-negative()` returns CSS calc function with the given `$value` multiplied by `-1`. 5 | */ 6 | @function var-negative($value) { 7 | @return calc((#{$value}) * -1); 8 | } 9 | -------------------------------------------------------------------------------- /src/db/migrations/V017__photo_fields.sql: -------------------------------------------------------------------------------- 1 | 2 | alter table photos add column width int not null default 0; 3 | alter table photos add column height int not null default 0; 4 | alter table photos add column description text not null default ''; 5 | alter table photos add column copyright text; -------------------------------------------------------------------------------- /front/src/lib/components/dives/GraphPlaceholder.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /front/src/routes/dives/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | 3 | import type { PageLoad } from './$types'; 4 | 5 | export const load: PageLoad = async () => { 6 | let dives = await client.getDives(); 7 | 8 | return { 9 | dives: dives.dives 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_transition-duration.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Transition duration CSS variable. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('transition-duration', $transition-duration); // 0.2s 10 | } 11 | -------------------------------------------------------------------------------- /front/src/style/spectre/_utilities.scss: -------------------------------------------------------------------------------- 1 | @forward 'utilities/colors'; 2 | @forward 'utilities/cursors'; 3 | @forward 'utilities/display'; 4 | @forward 'utilities/divider'; 5 | @forward 'utilities/loading'; 6 | @forward 'utilities/position'; 7 | @forward 'utilities/shapes'; 8 | @forward 'utilities/text'; 9 | -------------------------------------------------------------------------------- /templates/password_reset.txt: -------------------------------------------------------------------------------- 1 | Password Reset 2 | 3 | We have received a request to reset your password for DiveDB 4 | 5 | To reset your password, follow the link below: 6 | 7 | {{site_url}}/reset-password?token={{id}}&email={{email}} 8 | 9 | If you did not make this request, feel free to ignore this email -------------------------------------------------------------------------------- /divedb_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "divedb_core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "AGPL-3.0" 6 | 7 | 8 | [dependencies] 9 | anyhow = "1.0.27" 10 | tokio-postgres = {version = "0.7", features=["with-chrono-0_4", "with-uuid-0_8"]} 11 | divedb_macro = {path = "../divedb_macro"} -------------------------------------------------------------------------------- /front/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/comments.gql: -------------------------------------------------------------------------------- 1 | mutation addComment($diveId: UUID!, $description: String!) { 2 | newComment(comment: { description: $description, diveId: $diveId }) { 3 | ...Comment 4 | } 5 | } 6 | 7 | mutation removeComment($commentId: UUID!) { 8 | removeComment(id: $commentId) 9 | } 10 | -------------------------------------------------------------------------------- /front/src/routes/photos/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load = (async ({ url }) => { 5 | let photos = await getClient.getPhotos(); 6 | 7 | return { 8 | photos: photos.photos 9 | }; 10 | }) satisfies PageLoad; 11 | -------------------------------------------------------------------------------- /front/src/style/spectre/utilities/_shapes.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/get-var' as *; 2 | @use '../variables' as *; 3 | 4 | // Shapes 5 | .s-rounded { 6 | // border-radius: $border-radius; // old spectre.css 7 | border-radius: get-var('border-radius'); 8 | } 9 | 10 | .s-circle { 11 | border-radius: 50%; 12 | } 13 | -------------------------------------------------------------------------------- /front/src/routes/photos/edit/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ params }) => { 5 | let photos = await client.getPhotos({ id: params.id }); 6 | 7 | return { 8 | photo: photos.photos[0] 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /src/db/migrations/V016__passwords.sql: -------------------------------------------------------------------------------- 1 | create table if not exists password_reset ( 2 | id uuid primary key, 3 | user_id uuid not null REFERENCES users(id) ON DELETE CASCADE, 4 | "date" timestamp with time zone not null default now() 5 | ); 6 | 7 | 8 | alter table users add column email_verified boolean not null default false; -------------------------------------------------------------------------------- /templates/email_verification.txt: -------------------------------------------------------------------------------- 1 | Email Verification 2 | 3 | Welcome to DiveDB! Please verify your email address to complete your registration. 4 | 5 | To verify your email address, follow the link below: 6 | 7 | {{site_url}}/verify-email?token={{id}}&email={{email}} 8 | 9 | If you did not create an account with DiveDB, please ignore this email. -------------------------------------------------------------------------------- /front/src/routes/users/[username]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types'; 2 | import { client } from '$lib/graphql/client'; 3 | 4 | export const load: PageLoad = async ({ params }) => { 5 | let { user, siteUrl } = await client.getUser({ username: params.username }); 6 | 7 | return { 8 | user, 9 | siteUrl 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/db/migrations/V014__sealife_hide_location.sql: -------------------------------------------------------------------------------- 1 | alter table sealife add column hide_location boolean not null default false; 2 | 3 | -- This removes duplicates 4 | with u as (select distinct * from sealife_tags), x as (delete from sealife_tags) insert into sealife_tags select * from u; 5 | alter table sealife_tags add primary key (sealife_id, photo_id); -------------------------------------------------------------------------------- /front/src/lib/components/CheckLogin.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/photo.gql: -------------------------------------------------------------------------------- 1 | fragment PhotoSummary on Photo { 2 | id 3 | userId 4 | filename 5 | date 6 | size 7 | width 8 | height 9 | likes 10 | liked 11 | dive { 12 | ...DiveSummary 13 | } 14 | diveSite { 15 | ...SiteSummary 16 | } 17 | sealife { 18 | ...SealifeSummary 19 | } 20 | user { 21 | ...UserSummary 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /front/src/style/spectre/utilities/_cursors.scss: -------------------------------------------------------------------------------- 1 | // Cursors 2 | .c-hand { 3 | cursor: pointer; 4 | } 5 | 6 | .c-move { 7 | cursor: move; 8 | } 9 | 10 | .c-zoom-in { 11 | cursor: zoom-in; 12 | } 13 | 14 | .c-zoom-out { 15 | cursor: zoom-out; 16 | } 17 | 18 | .c-not-allowed { 19 | cursor: not-allowed; 20 | } 21 | 22 | .c-auto { 23 | cursor: auto; 24 | } 25 | -------------------------------------------------------------------------------- /front/src/lib/util/formatGas.ts: -------------------------------------------------------------------------------- 1 | import type { GasOutput } from "$lib/graphql/generated"; 2 | 3 | export default (gas: GasOutput) => { 4 | if (gas.o2 == 21. && gas.he == 0.) { 5 | return "Air" 6 | } else if (gas.he == 0.) { 7 | return `EAN${Math.trunc(gas.o2)}` 8 | } else { 9 | return `${Math.trunc(gas.o2)}%/${Math.trunc(gas.he)}%` 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_other-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Spectre.css other colors. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include define-color('code-color', $code-color); // #d73e48 10 | @include define-color('highlight-color', $highlight-color); // #ffe9b3 11 | } 12 | -------------------------------------------------------------------------------- /src/schema/password_reset.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::*; 2 | use chrono::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | use divedb_core::FromRow; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone, FromRow)] 9 | pub struct PasswordReset { 10 | pub id: Uuid, 11 | pub user_id: Uuid, 12 | pub date: DateTime, 13 | } 14 | -------------------------------------------------------------------------------- /front/src/routes/users/[username]/dives/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | 3 | import type { PageLoad } from './$types'; 4 | 5 | export const load: PageLoad = async ({ params }) => { 6 | let dives = await client.getDives({ username: params.username }); 7 | 8 | return { 9 | dives: dives.dives, 10 | username: params.username 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/schema/email_verification.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::*; 2 | use chrono::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | use divedb_core::FromRow; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone, FromRow)] 9 | pub struct EmailVerification { 10 | pub id: Uuid, 11 | pub user_id: Uuid, 12 | pub date: DateTime, 13 | } 14 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/likes.gql: -------------------------------------------------------------------------------- 1 | mutation likeDive($diveId: UUID!) { 2 | likeDive(diveId: $diveId) 3 | } 4 | 5 | mutation unlikeDive($diveId: UUID!) { 6 | unlikeDive(diveId: $diveId) 7 | } 8 | 9 | mutation likePhoto($photoId: UUID!) { 10 | likePhoto(photoId: $photoId) 11 | } 12 | 13 | mutation unlikePhoto($photoId: UUID!) { 14 | unlikePhoto(photoId: $photoId) 15 | } 16 | -------------------------------------------------------------------------------- /front/src/routes/users/[username]/photos/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ params }) => { 5 | let photos = await getClient.getPhotos({ username: params.username }); 6 | 7 | return { 8 | photos: photos.photos, 9 | username: params.username 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/index.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @forward 'avatar'; 3 | @forward 'box-shadow-side'; 4 | @forward 'button'; 5 | @forward 'clearfix'; 6 | @forward 'color'; 7 | @forward 'define-color-based-on'; 8 | @forward 'define-color'; 9 | @forward 'label'; 10 | @forward 'position'; 11 | @forward 'set-var'; 12 | @forward 'shadow'; 13 | @forward 'text'; 14 | @forward 'toast'; 15 | -------------------------------------------------------------------------------- /front/src/routes/admin/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | 3 | export const prerender = false; 4 | 5 | export async function load() { 6 | try { 7 | let { regions } = await client.getRegions(); 8 | let { feedback } = await client.getFeedback(); 9 | 10 | return { 11 | regions, 12 | feedback 13 | }; 14 | } catch (error) { 15 | return {}; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /front/src/style/spectre/spectre-icons.scss: -------------------------------------------------------------------------------- 1 | // Variables and mixins 2 | // @forward 'mixins'; 3 | // @forward 'variables'; 4 | @use 'variables' as *; 5 | 6 | /*! Spectre.css Icons v#{$version} | MIT License | github.com/picturepan2/spectre */ 7 | // Icons 8 | @forward 'icons/icons-core'; 9 | @forward 'icons/icons-navigation'; 10 | @forward 'icons/icons-action'; 11 | @forward 'icons/icons-object'; 12 | -------------------------------------------------------------------------------- /front/src/lib/icons/DiveSiteIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/diveSchedule.gql: -------------------------------------------------------------------------------- 1 | fragment DiveScheduleNode on DiveSchedule { 2 | runtime 3 | tts 4 | stages { 5 | ...DiveStageNode 6 | } 7 | smallChart 8 | bigChart 9 | } 10 | 11 | fragment DiveStageNode on DiveStage { 12 | stageType 13 | time 14 | depth 15 | gas { 16 | ...GasOutputNode 17 | } 18 | } 19 | 20 | fragment GasOutputNode on GasOutput { 21 | o2 22 | he 23 | } 24 | -------------------------------------------------------------------------------- /front/src/routes/sealife/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | 3 | export async function load() { 4 | try { 5 | let { sealife } = await getClient.getSealifeSummary(); 6 | let { categories } = await getClient.getCategories(); 7 | 8 | return { 9 | sealife, 10 | categories 11 | }; 12 | } catch (error) { 13 | return { 14 | categories: [] 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /front/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | process.env.BACKEND_URL = 'http://localhost:3333'; 6 | 7 | return { 8 | server: { 9 | proxy: { 10 | '/api': { 11 | target: 'http://localhost:3333' 12 | } 13 | } 14 | }, 15 | plugins: [sveltekit()] 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /src/db/migrations/V011__sealife_revisions.sql: -------------------------------------------------------------------------------- 1 | 2 | create table if not exists sealife_revisions ( 3 | sealife_id uuid REFERENCES sealife(id) ON DELETE SET NULL, 4 | name text not null, 5 | scientific_name text, 6 | description text not null, 7 | photo_id uuid REFERENCES photos(id) ON DELETE SET NULL, 8 | "date" timestamp with time zone not null, 9 | slug text not null 10 | ); 11 | -------------------------------------------------------------------------------- /front/src/lib/icons/BaseIcon.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {@render children?.()} 13 | 14 | -------------------------------------------------------------------------------- /front/src/lib/session.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export interface UserInfo { 4 | id: string; 5 | username: string; 6 | email: string; 7 | level: 'ADMIN' | 'EDITOR' | 'USER'; 8 | } 9 | // interface Locals {} 10 | // interface Platform {} 11 | export interface Session { 12 | loggedIn?: boolean; 13 | user?: UserInfo; 14 | } 15 | 16 | export const session = writable({}); 17 | -------------------------------------------------------------------------------- /front/src/routes/admin/region/edit/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const prerender = false; 5 | 6 | export const load: PageLoad = async ({ params }) => { 7 | let response = await client.getRegions(); 8 | 9 | let region = response.regions.find((val) => val.id == params.id); 10 | 11 | return { 12 | region 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /front/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | DiveDB - {page.status} 7 | 8 |
9 |
10 |
11 |

Error - {page.status}

12 |

{page.error?.message}

13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_border-width.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Border width. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('border-radius', $border-radius); // $unit-h 10 | @include set-var('border-width', $border-width); // $unit-o 11 | @include set-var('border-width', $border-width-lg, $suffix: 'lg'); // $unit-h 12 | } 13 | -------------------------------------------------------------------------------- /front/src/style/spectre/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @forward 'mixins/avatar'; 3 | @forward 'mixins/button'; 4 | @forward 'mixins/clearfix'; 5 | @forward 'mixins/color'; 6 | @forward 'mixins/define-color-based-on'; 7 | @forward 'mixins/define-color'; 8 | @forward 'mixins/label'; 9 | @forward 'mixins/position'; 10 | @forward 'mixins/set-var'; 11 | @forward 'mixins/shadow'; 12 | @forward 'mixins/text'; 13 | @forward 'mixins/toast'; 14 | -------------------------------------------------------------------------------- /front/src/routes/divesites/map/[[region]]/+page.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '$lib/graphql/client'; 2 | 3 | import type { PageLoad } from './$types'; 4 | 5 | export const load: PageLoad = async ({ params }) => { 6 | let response = await getClient.getDiveSites(); 7 | let diveSites = response?.diveSites; 8 | let regions = response?.regions; 9 | 10 | return { 11 | diveSites, 12 | regions, 13 | region: params.region 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /front/src/lib/icons/CheckIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/routes/dives/edit/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const prerender = false; 5 | 6 | export const load: PageLoad = async ({ params }) => { 7 | try { 8 | let response = await client.getDive({ id: params.id }); 9 | 10 | let dive = response.dives[0]; 11 | 12 | return { 13 | dive 14 | }; 15 | } catch (error) { 16 | return {}; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /front/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | %sveltekit.head% 11 | 12 | 13 | 14 |
%sveltekit.body%
15 | 16 | 17 | -------------------------------------------------------------------------------- /front/src/lib/components/labels/UserLabel.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {#if user.displayName != undefined} 13 | {user.displayName} 14 | {:else} 15 | @{user.username} 16 | {/if} 17 | 18 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_z-index.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | z-index. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('z-index-0', $zindex-0); // 1 10 | @include set-var('z-index-1', $zindex-1); // 100 11 | @include set-var('z-index-2', $zindex-2); // 200 12 | @include set-var('z-index-3', $zindex-3); // 300 13 | @include set-var('z-index-4', $zindex-4); // 400 14 | } 15 | -------------------------------------------------------------------------------- /front/src/lib/icons/HeartFilled.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/routes/divesites/edit/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const prerender = false; 5 | 6 | export const load: PageLoad = async ({ params }) => { 7 | try { 8 | let response = await client.getDiveSites({ id: params.id }); 9 | let diveSite = response?.diveSites[0]; 10 | 11 | return { 12 | diveSite 13 | }; 14 | } catch (error) { 15 | return {}; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/db/migrations/V013__watermark_features.sql: -------------------------------------------------------------------------------- 1 | 2 | DO $$ 3 | BEGIN 4 | IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'OverlayLocation') THEN 5 | CREATE TYPE "OverlayLocation" as enum ('TopLeft', 'TopRight', 'BottomLeft', 'BottomRight'); 6 | END IF; 7 | END$$; 8 | 9 | alter table users add column watermark_location "OverlayLocation" not null default 'BottomRight'; 10 | alter table users add column copyright_location "OverlayLocation" default 'BottomLeft'; -------------------------------------------------------------------------------- /front/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: '../schema.graphql' 2 | documents: './src/lib/graphql/**/*.gql' 3 | config: 4 | scalars: 5 | Datetime: string 6 | UUID: string 7 | generates: 8 | src/lib/graphql/generated.ts: 9 | plugins: 10 | - typescript 11 | - typescript-operations 12 | - typescript-graphql-request 13 | config: 14 | dedupeFragments: true 15 | useTypeImports: true 16 | 17 | hooks: 18 | afterAllFileWrite: 19 | - prettier --write 20 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_avatar.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/get-var' as *; 2 | @use '../variables' as *; 3 | 4 | // Avatar mixin 5 | // @mixin avatar-base($size: $unit-8) { // old spectre.css 6 | @mixin avatar-base($size: 'unit-8') { 7 | // font-size: $size * 0.5; // old spectre.css 8 | font-size: calc(get-var($size) * 0.5); 9 | // height: $size; // old spectre.css 10 | height: get-var($size); 11 | // width: $size; // old spectre.css 12 | width: get-var($size); 13 | } 14 | -------------------------------------------------------------------------------- /front/src/lib/icons/SearchIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/lib/util/fbRedirect.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | 3 | export const fbLoginRedirect = browser 4 | ? location.protocol + 5 | '//' + 6 | location.hostname + 7 | (location.port ? ':' + location.port : '') + 8 | '/facebook/login' 9 | : ''; 10 | 11 | export const fbRegisterRedirect = browser 12 | ? location.protocol + 13 | '//' + 14 | location.hostname + 15 | (location.port ? ':' + location.port : '') + 16 | '/facebook/register' 17 | : ''; 18 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/getDiveSites.gql: -------------------------------------------------------------------------------- 1 | query getDiveSites($id: UUID, $name: String, $maxDepth: Float, $slug: String) { 2 | diveSites(id: $id, name: $name, maxDepth: $maxDepth, slug: $slug) { 3 | ...Site 4 | } 5 | siteUrl 6 | regions { 7 | ...RegionNode 8 | } 9 | } 10 | query getDiveSitesSummaryMetrics($id: UUID, $name: String, $maxDepth: Float, $slug: String) { 11 | diveSites(id: $id, name: $name, maxDepth: $maxDepth, slug: $slug) { 12 | ...SiteSummaryMetrics 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /front/src/lib/icons/ChatFilled.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_body-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color' as *; 2 | @use '../mixins/define-color-based-on' as *; 3 | @use '../variables' as *; 4 | 5 | /* 6 | Spectre.css body colors. Alphabetical order. 7 | */ 8 | :root, 9 | :host { 10 | @include define-color('body-bg-color', $body-bg-color); // $body-bg-color 11 | @include define-color-based-on( 12 | 'body-font-color', 13 | 'dark-color', 14 | $lightness: +5% 15 | ); // lighten($dark-color, 5%) 16 | } 17 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/sealife.gql: -------------------------------------------------------------------------------- 1 | fragment SealifeNode on Sealife { 2 | id 3 | name 4 | summary 5 | scientificName 6 | description 7 | slug 8 | date 9 | categoryMap 10 | hideLocation 11 | photoId 12 | photo { 13 | ...PhotoSummary 14 | } 15 | latestPhotos { 16 | ...PhotoSummary 17 | } 18 | references { 19 | ...Reference 20 | } 21 | } 22 | 23 | fragment SealifeSummary on Sealife { 24 | id 25 | name 26 | scientificName 27 | summary 28 | slug 29 | photoId 30 | } 31 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/getPhotos.gql: -------------------------------------------------------------------------------- 1 | query getPhotos( 2 | $id: UUID 3 | $userId: UUID 4 | $username: String 5 | $diveSite: UUID 6 | $dive: UUID 7 | $sealifeId: UUID 8 | $offset: Int 9 | $orderByUpload: Boolean 10 | ) { 11 | photos( 12 | id: $id 13 | userId: $userId 14 | username: $username 15 | diveSite: $diveSite 16 | dive: $dive 17 | sealifeId: $sealifeId 18 | offset: $offset 19 | orderByUpload: $orderByUpload 20 | ) { 21 | ...PhotoSummary 22 | } 23 | siteUrl 24 | } 25 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/passwordReset.gql: -------------------------------------------------------------------------------- 1 | mutation requestResetToken($email: String!) { 2 | requestResetToken(email: $email) 3 | } 4 | 5 | mutation resetPassword($email: String!, $newPassword: String!, $token: UUID!) { 6 | resetPassword(email: $email, newPassword: $newPassword, token: $token) { 7 | ...CurrentUserToken 8 | } 9 | } 10 | 11 | mutation changePassword($oldPassword: String!, $newPassword: String!) { 12 | changePassword(oldPassword: $oldPassword, newPassword: $newPassword) 13 | } 14 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_set-var.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | @use '../variables' as *; 3 | 4 | /* 5 | The mixin `set-var()` defines the CSS variable with a specified name, value, prefix, and optional suffix. By default argument prefix is set to `$var-prefix`. 6 | */ 7 | @mixin set-var($name, $value: '', $prefix: $var-prefix, $suffix: '') { 8 | @if string.length($suffix) > 0 { 9 | --#{$prefix}-#{$name}-#{$suffix}: #{$value}; 10 | } @else { 11 | --#{$prefix}-#{$name}: #{$value}; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_control-size.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Control size. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('control-icon-size', $control-icon-size); // 0.8rem 10 | @include set-var('control-size', $control-size); // $unit-9 11 | @include set-var('control-size', $control-size-lg, $suffix: 'lg'); // $unit-10 12 | @include set-var('control-size', $control-size-sm, $suffix: 'sm'); // $unit-7 13 | } 14 | -------------------------------------------------------------------------------- /front/src/routes/sealife/edit/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ params }) => { 5 | try { 6 | let response = await client.getSealife({ id: params.id }); 7 | let sealife = response?.sealife[0]; 8 | let { categories } = await client.getCategories(); 9 | 10 | return { 11 | sealife, 12 | categories 13 | }; 14 | } catch (error) { 15 | return { 16 | categories: [] 17 | }; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /front/src/lib/components/FormRow.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | {@render children?.()} 17 |
18 |
19 | -------------------------------------------------------------------------------- /front/src/routes/dives/plan/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | DiveDB - Deco Planner 8 | 9 | 10 |
11 |
12 |
13 |

14 | Deco Planner 15 |

16 |
17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /front/src/lib/components/leaflet/Icon.svelte: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /front/src/style/spectre/_animations.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'functions/var-negative' as *; 3 | @use 'variables' as *; 4 | 5 | // Animations 6 | @keyframes loading { 7 | 0% { 8 | transform: rotate(0deg); 9 | } 10 | 100% { 11 | transform: rotate(360deg); 12 | } 13 | } 14 | 15 | @keyframes slide-down { 16 | 0% { 17 | opacity: 0; 18 | // transform: translateY(-$unit-8); // old spectre.css 19 | transform: translateY(var-negative(get-var('unit-8'))); 20 | } 21 | 100% { 22 | opacity: 1; 23 | transform: translateY(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /front/src/style/spectre/_hero.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'variables' as *; 3 | 4 | // Hero 5 | .hero { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | padding-bottom: 4rem; 10 | padding-top: 4rem; 11 | 12 | &.hero-sm { 13 | padding-bottom: 2rem; 14 | padding-top: 2rem; 15 | } 16 | 17 | &.hero-lg { 18 | padding-bottom: 8rem; 19 | padding-top: 8rem; 20 | } 21 | 22 | .hero-body { 23 | // padding: $layout-spacing; // old spectre.css 24 | padding: get-var('layout-spacing', $unit: 1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_bg-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color-based-on' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Spectre.css bg colors. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include define-color-based-on( 10 | 'bg-color', 11 | 'dark-color', 12 | $lightness: +75% 13 | ); // lighten($dark-color, 75%) 14 | @include define-color-based-on( 15 | 'bg-color-dark', 16 | 'bg-color', 17 | $lightness: -3% 18 | ); // darken($bg-color, 3%) 19 | @include define-color-based-on('bg-color-light', 'light-color'); // $light-color 20 | } 21 | -------------------------------------------------------------------------------- /front/src/style/spectre/functions/_color.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | 3 | /* 4 | The function `color()` returns the hsla color from a CSS variable of the given `$name`. 5 | */ 6 | @function color( 7 | $name, 8 | $hue: 0deg, 9 | $lightness: 0%, 10 | $saturation: 0%, 11 | $alpha: 1, 12 | $prefix: $var-prefix 13 | ) { 14 | @return hsla( 15 | calc(var(--#{$prefix}-#{$name}-h) + #{$hue}), 16 | calc(var(--#{$prefix}-#{$name}-s) + #{$saturation}), 17 | calc(var(--#{$prefix}-#{$name}-l) + #{$lightness}), 18 | calc(var(--#{$prefix}-#{$name}-a) * #{$alpha}) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_control-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Spectre.css control colors. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include define-color('disabled-color', $disabled-color); // ! New. $bg-color-dark 10 | @include define-color('error-color', $error-color); // #e85600 11 | @include define-color('info-color', $info-color); // #d9edf7 12 | @include define-color('success-color', $success-color); // #32b643 13 | @include define-color('warning-color', $warning-color); // #ffb700 14 | } 15 | -------------------------------------------------------------------------------- /front/src/style/spectre/spectre-exp.scss: -------------------------------------------------------------------------------- 1 | // Variables and mixins 2 | // @forward 'mixins'; 3 | // @forward 'variables'; 4 | @use 'variables' as *; 5 | 6 | /*! Spectre.css Experimentals v#{$version} | MIT License | github.com/picturepan2/spectre */ 7 | // Experimentals 8 | @forward 'autocomplete'; 9 | @forward 'calendars'; 10 | @forward 'carousels'; 11 | @forward 'comparison-sliders'; 12 | @forward 'filters'; 13 | @forward 'meters'; 14 | @forward 'off-canvas'; 15 | @forward 'parallax'; 16 | @forward 'progress'; 17 | @forward 'sliders'; 18 | @forward 'timelines'; 19 | @forward 'viewer-360'; 20 | -------------------------------------------------------------------------------- /front/src/lib/icons/DiveLogIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_layout-spacing.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/strip-unit' as *; 2 | @use '../mixins/set-var' as *; 3 | @use '../variables' as *; 4 | 5 | /* 6 | Layout spacing. Alphabetical order. 7 | */ 8 | :root, 9 | :host { 10 | @include set-var('layout-spacing', strip-unit($layout-spacing)); // $unit-2 11 | @include set-var('layout-spacing', strip-unit($layout-spacing-lg), $suffix: 'lg'); // $unit-4 12 | @include set-var('layout-spacing', strip-unit($layout-spacing-sm), $suffix: 'sm'); // $unit-1 13 | @include set-var('layout-spacing', 1rem, $suffix: 'unit'); // rem 14 | } 15 | -------------------------------------------------------------------------------- /src/db/migrations/V002__regions_dive_site_photo.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | alter table dive_sites add column photo_id uuid REFERENCES photos(id) ON DELETE SET NULL; 4 | alter table photos add column size int not null; 5 | alter table photos alter column "date" set default now(); 6 | alter table photos alter column "date" set not null; 7 | 8 | create table if not exists regions ( 9 | id uuid primary key, 10 | name text not null, 11 | 12 | lat_min double precision not null, 13 | lon_min double precision not null, 14 | lat_max double precision not null, 15 | lon_max double precision not null 16 | ) -------------------------------------------------------------------------------- /divedb_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | pub use divedb_macro::*; 3 | use tokio_postgres::Row; 4 | 5 | /// Really simple ORM for `tokio_postgres` 6 | pub trait FromRow { 7 | /// hydrate a struct from a database `Row` 8 | fn from_row(row: Row) -> Result 9 | where 10 | Self: std::marker::Sized; 11 | 12 | /// hydrate a `Vec` of structs from database `Row` list 13 | fn from_rows(rows: Vec) -> Result, Error> 14 | where 15 | Self: std::marker::Sized, 16 | { 17 | rows.into_iter().map(Self::from_row).collect() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /front/src/lib/graphql/queries/getSealife.gql: -------------------------------------------------------------------------------- 1 | query getSealifeSummary( 2 | $id: UUID 3 | $name: String 4 | $scientificName: String 5 | $slug: String 6 | $categoryValues: [UUID!] 7 | ) { 8 | sealife( 9 | id: $id 10 | name: $name 11 | scientificName: $scientificName 12 | slug: $slug 13 | categoryValues: $categoryValues 14 | ) { 15 | ...SealifeSummary 16 | } 17 | } 18 | 19 | query getSealife($id: UUID, $name: String, $scientificName: String, $slug: String) { 20 | sealife(id: $id, name: $name, scientificName: $scientificName, slug: $slug) { 21 | ...SealifeNode 22 | } 23 | siteUrl 24 | } 25 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_link-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color-based-on' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Spectre.css link colors. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include define-color-based-on('link-color', 'primary-color'); // $primary-color 10 | @include define-color-based-on( 11 | 'link-color-dark', 12 | 'link-color', 13 | $lightness: -10% 14 | ); // darken($link-color, 10%) 15 | @include define-color-based-on( 16 | 'link-color-light', 17 | 'link-color', 18 | $lightness: +10% 19 | ); // lighten($link-color, 10%) 20 | } 21 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_control-width.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Control width. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('control-width', $control-width-lg, $suffix: 'lg'); // 960px 10 | @include set-var('control-width', $control-width-md, $suffix: 'md'); // 640px 11 | @include set-var('control-width', $control-width-sm, $suffix: 'sm'); // 320px 12 | @include set-var('control-width', $control-width-xl, $suffix: 'xl'); // 1280px 13 | @include set-var('control-width', $control-width-xs, $suffix: 'xs'); // 180px 14 | } 15 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_shadow.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/color' as *; 2 | 3 | // Component focus shadow 4 | // @mixin control-shadow($color: $primary-color) { // old spectre.css 5 | @mixin control-shadow($color: 'primary-color') { 6 | // box-shadow: 0 0 0 .1rem rgba($color, .2); // old spectre.css 7 | box-shadow: 0 0 0 0.1rem color($color, $alpha: 0.2); 8 | } 9 | 10 | // Shadow mixin 11 | @mixin shadow-variant($offset) { 12 | // box-shadow: 0 $offset ($offset + .05rem) * 2 rgba($dark-color, .15); // old spectre.css 13 | box-shadow: 0 $offset ($offset + 0.05rem) * 2 color('dark-color', $alpha: 0.15); 14 | } 15 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | pub mod activitypub; 2 | mod categories; 3 | mod comment; 4 | mod dive; 5 | mod dive_site; 6 | mod dive_plan; 7 | mod email_verification; 8 | mod feedback; 9 | mod og_reference; 10 | mod password_reset; 11 | mod photo; 12 | mod region; 13 | mod sealife; 14 | mod user; 15 | 16 | pub use categories::*; 17 | pub use comment::*; 18 | pub use dive::*; 19 | pub use dive_site::*; 20 | pub use dive_plan::*; 21 | pub use email_verification::*; 22 | pub use feedback::*; 23 | pub use og_reference::*; 24 | pub use password_reset::*; 25 | pub use photo::*; 26 | pub use region::*; 27 | pub use sealife::*; 28 | pub use user::*; 29 | -------------------------------------------------------------------------------- /front/src/lib/graphql/mutations/loginUser.gql: -------------------------------------------------------------------------------- 1 | mutation loginUser($email: String!, $password: String!) { 2 | login(email: $email, password: $password) { 3 | ...CurrentUserToken 4 | } 5 | } 6 | 7 | mutation updateSettings( 8 | $displayName: String 9 | $watermarkLocation: OverlayLocation! 10 | $copyrightLocation: OverlayLocation 11 | $description: String! 12 | $photoId: UUID 13 | ) { 14 | updateSettings( 15 | displayName: $displayName 16 | watermarkLocation: $watermarkLocation 17 | copyrightLocation: $copyrightLocation 18 | description: $description 19 | photoId: $photoId 20 | ) { 21 | ...CurrentUser 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/index.scss: -------------------------------------------------------------------------------- 1 | @forward 'bg-colors'; 2 | @forward 'body-colors'; 3 | @forward 'border-colors'; 4 | @forward 'border-width'; 5 | @forward 'color-scheme'; 6 | @forward 'control-colors'; 7 | @forward 'control-padding'; 8 | @forward 'control-size'; 9 | @forward 'control-width'; 10 | @forward 'core-colors'; 11 | @forward 'font-size'; 12 | @forward 'gray-colors'; 13 | @forward 'layout-spacing'; 14 | @forward 'link-colors'; 15 | @forward 'other-colors'; 16 | @forward 'parallax'; 17 | @forward 'responsive-breakpoints'; 18 | @forward 'transition-duration'; 19 | @forward 'unit-sizes'; 20 | @forward 'z-index'; 21 | -------------------------------------------------------------------------------- /front/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:22 AS builder 3 | 4 | WORKDIR /front 5 | 6 | COPY package.json yarn.lock /front/ 7 | 8 | RUN yarn install --frozen-lockfile 9 | 10 | COPY svelte.config.js vite.config.ts tsconfig.json /front/ 11 | COPY static /front/static 12 | COPY src /front/src 13 | 14 | RUN yarn build 15 | 16 | FROM node:22-slim AS production 17 | 18 | WORKDIR /front 19 | 20 | COPY package.json yarn.lock /front/ 21 | 22 | RUN yarn install --frozen-lockfile --production && yarn cache clean 23 | 24 | COPY --from=builder /front/build /front/build 25 | COPY --from=builder /front/static /front/static 26 | 27 | ENTRYPOINT ["node", "build"] 28 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_gray-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color-based-on' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Spectre.css gray colors. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include define-color-based-on( 10 | 'gray-color', 11 | 'dark-color', 12 | $lightness: +55% 13 | ); // lighten($dark-color, 55%) 14 | @include define-color-based-on( 15 | 'gray-color-dark', 16 | 'gray-color', 17 | $lightness: -30% 18 | ); // darken($gray-color, 30%) 19 | @include define-color-based-on( 20 | 'gray-color-light', 21 | 'gray-color', 22 | $lightness: +20% 23 | ); // lighten($gray-color, 20%) 24 | } 25 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_font-size.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Font size and line height. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | // Font sizes. 10 | @include set-var('font-size', $font-size); // 0.8rem 11 | @include set-var('font-size', $font-size-lg, $suffix: 'lg'); // 0.9rem 12 | @include set-var('font-size', $font-size-sm, $suffix: 'sm'); // 0.7rem 13 | @include set-var('html-font-size', $html-font-size); // 20px 14 | 15 | // Line height. 16 | @include set-var('html-line-height', $html-line-height); // 1.5 17 | @include set-var('line-height', $line-height); // 1.2rem 18 | } 19 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_border-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color-based-on' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Spectre.css border colors. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include define-color-based-on( 10 | 'border-color', 11 | 'dark-color', 12 | $lightness: +65% 13 | ); // lighten($dark-color, 65%) 14 | @include define-color-based-on( 15 | 'border-color-dark', 16 | 'border-color', 17 | $lightness: -10% 18 | ); // darken($border-color, 10%) 19 | @include define-color-based-on( 20 | 'border-color-light', 21 | 'border-color', 22 | $lightness: +8% 23 | ); // lighten($border-color, 8%) 24 | } 25 | -------------------------------------------------------------------------------- /front/src/lib/util/imageAlt.ts: -------------------------------------------------------------------------------- 1 | import type { PhotoSummaryFragment } from '$lib/graphql/generated'; 2 | 3 | export default function imageAlt(image: PhotoSummaryFragment, includeDate = false) { 4 | let imageAlt = ''; 5 | const diveSite = image.diveSite; 6 | 7 | if (image.sealife) { 8 | imageAlt = `Photo of ${image.sealife.name}`; 9 | if (diveSite) { 10 | imageAlt += ` at ${diveSite.name}. `; 11 | } else { 12 | imageAlt += `. `; 13 | } 14 | } else if (diveSite) { 15 | imageAlt += `Photo at ${diveSite.name}. `; 16 | } 17 | 18 | if (includeDate && image.date) { 19 | imageAlt += `Taken on ${new Date(image.date).toLocaleString()}`; 20 | } 21 | 22 | return imageAlt; 23 | } 24 | -------------------------------------------------------------------------------- /src/schema/region.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::*; 2 | use divedb_core::FromRow; 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Serialize, Deserialize, Debug, Clone, FromRow, SimpleObject)] 7 | pub struct Region { 8 | pub id: Uuid, 9 | pub name: String, 10 | pub lat_min: f64, 11 | pub lon_min: f64, 12 | pub lat_max: f64, 13 | pub lon_max: f64, 14 | pub slug: String, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Clone, FromRow, InputObject)] 18 | pub struct CreateRegion { 19 | pub id: Option, 20 | pub name: String, 21 | pub lat_min: f64, 22 | pub lon_min: f64, 23 | pub lat_max: f64, 24 | pub lon_max: f64, 25 | } 26 | -------------------------------------------------------------------------------- /front/src/lib/icons/LinkIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /front/src/style/spectre/_navbar.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'variables' as *; 3 | 4 | // Navbar 5 | .navbar { 6 | align-items: stretch; 7 | display: flex; 8 | flex-wrap: wrap; 9 | justify-content: space-between; 10 | 11 | .navbar-section { 12 | align-items: center; 13 | display: flex; 14 | flex: 1 0 0; 15 | 16 | &:not(:first-child):last-child { 17 | justify-content: flex-end; 18 | } 19 | } 20 | 21 | .navbar-center { 22 | align-items: center; 23 | display: flex; 24 | flex: 0 0 auto; 25 | } 26 | 27 | .navbar-brand { 28 | // font-size: $font-size-lg; // old spectre.css 29 | font-size: get-var('font-size', $suffix: 'lg'); 30 | text-decoration: none; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/user.gql: -------------------------------------------------------------------------------- 1 | fragment CurrentUser on LoginResponse { 2 | id 3 | email 4 | level 5 | username 6 | displayName 7 | watermarkLocation 8 | copyrightLocation 9 | description 10 | photoId 11 | emailVerified 12 | } 13 | 14 | fragment CurrentUserToken on LoginResponse { 15 | id 16 | email 17 | username 18 | level 19 | token 20 | } 21 | 22 | fragment UserSummary on PublicUserInfo { 23 | id 24 | username 25 | displayName 26 | } 27 | 28 | fragment UserInfo on PublicUserInfo { 29 | id 30 | username 31 | displayName 32 | description 33 | photoId 34 | photoCount 35 | diveCount 36 | latestPhotos { 37 | ...PhotoSummary 38 | } 39 | latestDives { 40 | ...DiveWithMetrics 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_parallax.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color' as *; 2 | @use '../mixins/set-var' as *; 3 | @use '../variables' as *; 4 | 5 | /* 6 | Parallax CSS variables. Alphabetical order. 7 | */ 8 | :root, 9 | :host { 10 | @include set-var('parallax-deg', $parallax-deg); // 3deg 11 | @include define-color( 12 | 'parallax-fade-color', 13 | $parallax-fade-color, 14 | $alpha: 0.35 15 | ); // rgba(255, 255, 255, 0.35) 16 | @include set-var('parallax-offset', $parallax-offset); // 4.5px 17 | @include set-var('parallax-offset-z', $parallax-offset-z); // 50px 18 | @include set-var('parallax-perspective', $parallax-perspective); // 1000px 19 | @include set-var('parallax-scale', $parallax-scale); // 0.95 20 | } 21 | -------------------------------------------------------------------------------- /front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /front/src/lib/components/categories/CategoryView.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#each categories as category, catIdx} 16 | {#if map[category.id] !== undefined} 17 | {category.name}: 18 | {map[category.id] 19 | .map((catId) => values.find((val) => val.id == catId)?.value) 20 | .join(', ')}{#if catIdx !== categories.length - 1}, {/if} 21 | {/if} 22 | {/each} 23 | -------------------------------------------------------------------------------- /front/src/lib/icons/ChatOutline.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/lib/components/dives/GraphImage.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {#if !smallOnly} 15 | 16 | 17 | {/if} 18 | {'Dive 26 | 27 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_define-color.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | @use 'set-var' as *; 3 | 4 | // The mixin defines CSS variable color in hsl form. 5 | @mixin define-color($name, $color, $prefix: $var-prefix, $alpha: -1) { 6 | @include set-var($name, hue($color), $prefix, 'h'); 7 | @include set-var($name, saturation($color), $prefix, 's'); 8 | @include set-var($name, lightness($color), $prefix, 'l'); 9 | @if $alpha == -1 { 10 | @include set-var($name, alpha($color), $prefix, 'a'); 11 | } @else { 12 | @include set-var($name, $alpha, $prefix, 'a'); 13 | } 14 | } 15 | 16 | // Old @angular-package/spectre.css 17 | // --s-#{$name}-h: #{hue($color)}; 18 | // --s-#{$name}-l: #{lightness($color)}; 19 | // --s-#{$name}-s: #{saturation($color)}; 20 | // --s-#{$name}-a: #{alpha($color)}; 21 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_responsive-breakpoints.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Responsive breakpoints. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('size-2x', $size-2x); // 1440px 10 | @include set-var('size-lg', $size-lg); // 960px 11 | @include set-var('size-md', $size-md); // 840px 12 | @include set-var('size-sm', $size-sm); // 600px 13 | @include set-var('size-xl', $size-xl); // 1280px 14 | @include set-var('size-xs', $size-xs); // 480px 15 | 16 | // OffCanvas breakpoint. 17 | // TODO: @media 18 | // @include set-var('off-canvas-breakpoint', $off-canvas-breakpoint); // $size-lg 19 | 20 | // Responsive breakpoint. 21 | @include set-var('responsive-breakpoint', $responsive-breakpoint); // $size-xs 22 | } 23 | -------------------------------------------------------------------------------- /front/src/style/spectre/functions/_get-var.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | @use '../variables' as *; 3 | 4 | /* 5 | The function returns the CSS var function or calc function with the specified name, prefix, optional suffix, or unit. Unit is used for variables with a separated unit from the value. 6 | */ 7 | @function get-var($name, $prefix: $var-prefix, $suffix: '', $unit: 0) { 8 | @if string.length($suffix) > 0 { 9 | @if $unit == 1 { 10 | @return calc(var(--#{$prefix}-#{$name}-#{$suffix}) * var(--#{$prefix}-#{$name}-unit)); 11 | } @else { 12 | @return var(--#{$prefix}-#{$name}-#{$suffix}); 13 | } 14 | } @else { 15 | @if $unit == 1 { 16 | @return calc(var(--#{$prefix}-#{$name}) * var(--#{$prefix}-#{$name}-unit)); 17 | } @else { 18 | @return var(--#{$prefix}-#{$name}); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_toast.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/color' as *; 2 | 3 | // Toast variant mixin 4 | /* 5 | The mixin includes toast `background` and `border` of a given `$color`. 6 | */ 7 | // @mixin toast-variant($color: $dark-color) { // old spectre.css 8 | @mixin toast-variant($color: 'dark-color') { 9 | // background: rgba($color, .95); // old spectre.css 10 | background: color($color, $alpha: 0.95); 11 | // border-color: $color; // old spectre.css 12 | border-color: color($color); 13 | } 14 | 15 | /* 16 | The mixin includes an extension class of name prefixed with `toast-` with a given color name that includes a toast variant of the given `$color`. 17 | */ 18 | @mixin toast-class-variant($name: 'dark', $color: 'dark-color') { 19 | &.toast-#{$name} { 20 | @include toast-variant($color); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /front/src/lib/components/dives/DiveComment.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 0}> 15 | {#if numComments == 0} 16 | 17 | {:else} 18 | 19 | {/if} 20 | {numComments > 0 ? numComments : ''} 21 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /front/src/style/spectre/utilities/_display.scss: -------------------------------------------------------------------------------- 1 | // Display 2 | .d-block { 3 | display: block; 4 | } 5 | .d-inline { 6 | display: inline; 7 | } 8 | .d-inline-block { 9 | display: inline-block; 10 | } 11 | .d-flex { 12 | display: flex; 13 | } 14 | .d-inline-flex { 15 | display: inline-flex; 16 | } 17 | .d-none, 18 | .d-hide { 19 | display: none !important; 20 | } 21 | .d-visible { 22 | visibility: visible; 23 | } 24 | .d-invisible { 25 | visibility: hidden; 26 | } 27 | .text-hide { 28 | background: transparent; 29 | border: 0; 30 | color: transparent; 31 | font-size: 0; 32 | line-height: 0; 33 | text-shadow: none; 34 | } 35 | .text-assistive { 36 | border: 0; 37 | clip: rect(0, 0, 0, 0); 38 | height: 1px; 39 | margin: -1px; 40 | overflow: hidden; 41 | padding: 0; 42 | position: absolute; 43 | width: 1px; 44 | } 45 | -------------------------------------------------------------------------------- /front/src/lib/components/photos/PhotoSlide.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | { 19 | client.updatePhoto({ photo: newPhoto }).then((val) => { 20 | photo = val.updatePhoto; 21 | onEditSave(val.updatePhoto); 22 | }); 23 | }} 24 | {photo} 25 | /> 26 |
27 | 28 | 33 | -------------------------------------------------------------------------------- /front/src/lib/components/leaflet/EventBridge.ts: -------------------------------------------------------------------------------- 1 | export default class EventBridge { 2 | entity: any; 3 | eventHandlers: any[]; 4 | 5 | constructor(entity: any, dispatch: any, events: any[] = []) { 6 | this.entity = entity; 7 | 8 | this.eventHandlers = []; 9 | if (events) { 10 | const eventMap: any = {}; 11 | events.forEach((event) => { 12 | if (!(event in eventMap)) { 13 | const handler = function (e: any) { 14 | dispatch(event, e); 15 | }; 16 | this.eventHandlers.push({ 17 | event: event, 18 | handler: handler 19 | }); 20 | entity.on(event, handler); 21 | eventMap[event] = handler; 22 | } 23 | }); 24 | } 25 | } 26 | 27 | unregister() { 28 | this.eventHandlers.forEach((entry) => { 29 | this.entity.off(entry.event, entry.handler); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/diveSite.gql: -------------------------------------------------------------------------------- 1 | fragment Site on DiveSite { 2 | id 3 | name 4 | description 5 | summary 6 | access 7 | difficulty 8 | depth 9 | lat 10 | lon 11 | published 12 | userId 13 | slug 14 | date 15 | siteMetrics { 16 | ...SiteMetricNode 17 | } 18 | photoId 19 | photo { 20 | ...PhotoSummary 21 | } 22 | latestPhotos { 23 | ...PhotoSummary 24 | } 25 | latestDives { 26 | ...DiveWithMetrics 27 | } 28 | references { 29 | ...Reference 30 | } 31 | } 32 | 33 | fragment SiteSummary on DiveSite { 34 | name 35 | id 36 | slug 37 | } 38 | 39 | fragment SiteMetricNode on SiteMetric { 40 | photoCount 41 | diveCount 42 | } 43 | 44 | fragment SiteSummaryMetrics on DiveSite { 45 | id 46 | name 47 | summary 48 | slug 49 | siteMetrics { 50 | ...SiteMetricNode 51 | } 52 | lat 53 | lon 54 | photoId 55 | } 56 | -------------------------------------------------------------------------------- /front/src/lib/icons/PhotoIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /front/src/lib/components/SiteMetrics.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | {site.siteMetrics.photoCount} Photos, 16 | 17 | 18 | 19 | 20 | 21 | {site.siteMetrics.diveCount} Dives Logged 22 | 23 | 24 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_define-color-based-on.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | @use '../functions/get-var' as *; 3 | @use 'set-var' as *; 4 | 5 | // Defines a color based on the specified CSS variable and its lightness. 6 | @mixin define-color-based-on($name, $color, $lightness: 0%, $prefix: $var-prefix) { 7 | @include set-var($name, get-var($color, $suffix: 'h'), $prefix, 'h'); 8 | @include set-var($name, get-var($color, $suffix: 's'), $prefix, 's'); 9 | @include set-var($name, calc(var(--#{$prefix}-#{$color}-l) + #{$lightness}), $prefix, 'l'); 10 | @include set-var($name, get-var($color, $suffix: 'a'), $prefix, 'a'); 11 | } 12 | 13 | // Old @angular-package/spectre.css 14 | // --s-#{$name}-h: var(--s-#{$color}-h); 15 | // --s-#{$name}-s: var(--s-#{$color}-s); 16 | // --s-#{$name}-l: calc(var(--s-#{$color}-l) + #{$lightness}); 17 | // --s-#{$name}-a: var(--s-#{$color}-a); 18 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_box-shadow-side.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/color' as *; 2 | 3 | /* 4 | The mixin includes the `box-shadow` of the specified side, size, and color. 5 | The side can be `bottom`, `left`, `right`, `top`, size default is 10px and color default is `gray-color`. 6 | */ 7 | @mixin box-shadow-side($side, $size: 10px, $color: 'gray-color') { 8 | @if $side == right { 9 | // Right side. 10 | box-shadow: $size 0 $size ($size * -1) color($color); 11 | } @else if $side == left { 12 | // Left side. 13 | box-shadow: ($size * -1) 0 $size ($size * -1) color($color); 14 | } @else if $side == bottom { 15 | // Bottom side. 16 | box-shadow: 0 $size $size ($size * -1) color($color); 17 | } @else if $side == top { 18 | // Top side. 19 | box-shadow: 0 ($size * -1) $size ($size * -1) color($color); 20 | } @else { 21 | @error "Unknown side #{$side}."; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /front/src/style/spectre/_panels.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Panels 6 | .panel { 7 | // border: $border-width solid $border-color; // old spectre.css 8 | border: get-var('border-width') solid color('border-color'); 9 | // border-radius: $border-radius; // old spectre.css 10 | border-radius: get-var('border-radius'); 11 | display: flex; 12 | flex-direction: column; 13 | 14 | .panel-header, 15 | .panel-footer { 16 | flex: 0 0 auto; 17 | // padding: $layout-spacing-lg; // old spectre.css 18 | padding: get-var('layout-spacing', $suffix: 'lg', $unit: 1); 19 | } 20 | 21 | .panel-nav { 22 | flex: 0 0 auto; 23 | } 24 | 25 | .panel-body { 26 | flex: 1 1 auto; 27 | overflow-y: auto; 28 | // padding: 0 $layout-spacing-lg; // old spectre.css 29 | padding: 0 get-var('layout-spacing', $suffix: 'lg', $unit: 1); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /front/src/lib/components/dives/DiveGraph.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if dive.hasMetrics} 17 |
18 | {#if link} 19 | 20 | 21 | 22 | {:else} 23 | 24 | {/if} 25 |
26 | {:else if link} 27 | 28 | 29 | 30 | {:else} 31 | 32 | {/if} 33 | -------------------------------------------------------------------------------- /front/src/style/spectre/_asian.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'variables' as *; 3 | 4 | // Optimized for East Asian CJK 5 | html:lang(zh), 6 | html:lang(zh-Hans), 7 | .lang-zh, 8 | .lang-zh-hans { 9 | font-family: $cjk-zh-hans-font-family; 10 | } 11 | 12 | html:lang(zh-Hant), 13 | .lang-zh-hant { 14 | font-family: $cjk-zh-hant-font-family; 15 | } 16 | 17 | html:lang(ja), 18 | .lang-ja { 19 | font-family: $cjk-jp-font-family; 20 | } 21 | 22 | html:lang(ko), 23 | .lang-ko { 24 | font-family: $cjk-ko-font-family; 25 | } 26 | 27 | :lang(zh), 28 | :lang(ja), 29 | .lang-cjk { 30 | ins, 31 | u { 32 | // border-bottom: $border-width solid; // old spectre.css 33 | border-bottom: get-var('border-width') solid; 34 | text-decoration: none; 35 | } 36 | 37 | del + del, 38 | del + s, 39 | ins + ins, 40 | ins + u, 41 | s + del, 42 | s + s, 43 | u + ins, 44 | u + u { 45 | margin-left: 0.125em; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /front/src/routes/sealife/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | import { unified } from 'unified'; 4 | import remarkParse from 'remark-parse'; 5 | import remarkRehype from 'remark-rehype'; 6 | import rehypeSanitize from 'rehype-sanitize'; 7 | import rehypeStringify from 'rehype-stringify'; 8 | 9 | export const load: PageLoad = async ({ params }) => { 10 | let response = await client.getSealife({ slug: params.slug }); 11 | let { categories } = await client.getCategories(); 12 | 13 | let sealife = response?.sealife[0]; 14 | let siteUrl = response.siteUrl; 15 | 16 | const mdProc = unified() 17 | .use(remarkParse) 18 | .use(remarkRehype) 19 | .use(rehypeSanitize) 20 | .use(rehypeStringify); 21 | 22 | const mdDesc = await mdProc.process(sealife.description); 23 | 24 | return { 25 | sealife, 26 | mdDesc, 27 | siteUrl, 28 | categories 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /front/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { sveltePreprocess } from 'svelte-preprocess'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: sveltePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter() 15 | }, 16 | onwarn: (warning, handler) => { 17 | if (warning.code.startsWith('a11y')) return; 18 | if (warning.code === 'element_invalid_self_closing_tag') return; 19 | 20 | handler(warning); 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /front/src/style/spectre/_viewer-360.scss: -------------------------------------------------------------------------------- 1 | // Mixin: Viewer slider sizes 2 | @use 'sass:math'; 3 | @use 'variables' as *; 4 | 5 | // 360 Degree Viewer 6 | 7 | // Mixin: Viewer slider sizes 8 | @mixin viewer-slider-size($image-number: 36) { 9 | @for $s from 1 through ($image-number) { 10 | .viewer-slider[max='#{$image-number}'][value='#{$s}'] + .viewer-image { 11 | background-position-y: percentage(math.div((($s)-1) * 1, ($image-number)-1)); 12 | } 13 | } 14 | } 15 | 16 | .viewer-360 { 17 | align-items: center; 18 | display: flex; 19 | flex-direction: column; 20 | 21 | // Copy and add more numbers if you need 22 | @include viewer-slider-size(36); 23 | 24 | .viewer-slider { 25 | cursor: ew-resize; 26 | margin: 1rem; 27 | order: 2; 28 | width: 60%; 29 | } 30 | 31 | .viewer-image { 32 | background-position-y: 0; 33 | background-repeat: no-repeat; 34 | background-size: 100%; 35 | max-width: 100%; 36 | order: 1; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/db/migrations/V015__open_graph_references.sql: -------------------------------------------------------------------------------- 1 | create table if not exists og_reference ( 2 | id uuid primary key, 3 | url text not null, 4 | title text not null, 5 | image_url text, 6 | description text not null, 7 | last_fetched timestamp with time zone not null default now() 8 | ); 9 | 10 | create table if not exists sealife_og_reference ( 11 | sealife_id uuid not null REFERENCES sealife(id) ON DELETE CASCADE, 12 | og_reference_id uuid not null REFERENCES og_reference(id) ON DELETE CASCADE 13 | ); 14 | 15 | create table if not exists dive_sites_og_reference ( 16 | dive_site_id uuid not null REFERENCES dive_sites(id) ON DELETE CASCADE, 17 | og_reference_id uuid not null REFERENCES og_reference(id) ON DELETE CASCADE 18 | ); 19 | 20 | create index if not exists sealife_og_reference_id on sealife_og_reference (sealife_id); 21 | create index if not exists dive_sites_og_reference_id on dive_sites_og_reference (dive_site_id); -------------------------------------------------------------------------------- /front/src/lib/graphql/fragments/dive.gql: -------------------------------------------------------------------------------- 1 | fragment DiveSummary on Dive { 2 | id 3 | date 4 | number 5 | numComments 6 | user { 7 | ...UserSummary 8 | } 9 | diveSite { 10 | ...SiteSummary 11 | } 12 | } 13 | 14 | fragment DiveWithMetrics on Dive { 15 | id 16 | userId 17 | date 18 | depth 19 | duration 20 | number 21 | hasMetrics 22 | summary 23 | likes 24 | liked 25 | numComments 26 | diveSite { 27 | ...SiteSummary 28 | } 29 | latestPhotos { 30 | ...PhotoSummary 31 | } 32 | user { 33 | ...UserSummary 34 | } 35 | } 36 | 37 | fragment DiveNode on Dive { 38 | id 39 | userId 40 | date 41 | depth 42 | duration 43 | number 44 | hasMetrics 45 | description 46 | summary 47 | published 48 | likes 49 | liked 50 | numComments 51 | comments { 52 | ...Comment 53 | } 54 | user { 55 | ...UserSummary 56 | } 57 | latestPhotos { 58 | ...PhotoSummary 59 | } 60 | diveSiteId 61 | diveSite { 62 | ...SiteSummary 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /front/src/lib/icons/HeartOutline.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_unit-sizes.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/set-var' as *; 2 | @use '../variables' as *; 3 | 4 | /* 5 | Unit sizes. Alphabetical order. 6 | */ 7 | :root, 8 | :host { 9 | @include set-var('unit-o', $unit-o); // 0.05rem 10 | @include set-var('unit-h', $unit-h); // 0.1rem 11 | 12 | @include set-var('unit-0', 0rem); // ! New. 0rem 13 | @include set-var('unit-1', $unit-1); // 0.2rem 14 | @include set-var('unit-2', $unit-2); // 0.4rem 15 | @include set-var('unit-3', $unit-3); // 0.6rem 16 | @include set-var('unit-4', $unit-4); // 0.8rem 17 | @include set-var('unit-5', $unit-5); // 1rem 18 | @include set-var('unit-6', $unit-6); // 1.2rem 19 | @include set-var('unit-7', $unit-7); // 1.4rem 20 | @include set-var('unit-8', $unit-8); // 1.6rem 21 | @include set-var('unit-9', $unit-9); // 1.8rem 22 | @include set-var('unit-10', $unit-10); // 2rem 23 | @include set-var('unit-12', $unit-12); // 2.4rem 24 | @include set-var('unit-16', $unit-16); // 3.2rem 25 | } 26 | -------------------------------------------------------------------------------- /front/src/routes/dives/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |

31 | New Dive 32 |

33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /front/src/routes/sites/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { PageLoad } from './$types'; 3 | import { unified } from 'unified'; 4 | import remarkParse from 'remark-parse'; 5 | import remarkRehype from 'remark-rehype'; 6 | import rehypeSanitize from 'rehype-sanitize'; 7 | import rehypeStringify from 'rehype-stringify'; 8 | 9 | export const load: PageLoad = async ({ params }) => { 10 | try { 11 | let response = await client.getDiveSites({ slug: params.slug }); 12 | let diveSite = response?.diveSites[0]; 13 | let siteUrl = response?.siteUrl; 14 | 15 | const mdProc = unified() 16 | .use(remarkParse) 17 | .use(remarkRehype) 18 | .use(rehypeSanitize) 19 | .use(rehypeStringify); 20 | 21 | const mdDesc = await mdProc.process(diveSite.description); 22 | const mdAccess = await mdProc.process(diveSite.access); 23 | 24 | return { 25 | diveSite, 26 | siteUrl, 27 | mdDesc, 28 | mdAccess 29 | }; 30 | } catch (error) { 31 | return {}; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /front/src/style/spectre/_tiles.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'mixins/text.scss' as *; 3 | @use 'variables' as *; 4 | 5 | // Tiles 6 | .tile { 7 | align-content: space-between; 8 | align-items: flex-start; 9 | display: flex; 10 | 11 | .tile-icon, 12 | .tile-action { 13 | flex: 0 0 auto; 14 | } 15 | .tile-content { 16 | flex: 1 1 auto; 17 | &:not(:first-child) { 18 | // padding-left: $unit-2; // old spectre.css 19 | padding-left: get-var('unit-2'); 20 | } 21 | &:not(:last-child) { 22 | // padding-right: $unit-2; // old spectre.css 23 | padding-right: get-var('unit-2'); 24 | } 25 | } 26 | .tile-title, 27 | .tile-subtitle { 28 | // line-height: $line-height; // old spectre.css 29 | line-height: get-var('line-height'); 30 | } 31 | 32 | &.tile-centered { 33 | align-items: center; 34 | 35 | .tile-content { 36 | overflow: hidden; 37 | } 38 | 39 | .tile-title, 40 | .tile-subtitle { 41 | @include text-ellipsis(); 42 | margin-bottom: 0; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/db/migrations/external_sql.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use async_trait::async_trait; 3 | 4 | use super::Migration; 5 | use deadpool_postgres::Pool; 6 | 7 | pub struct ExternalMigration { 8 | sql_name: &'static str, 9 | sql_contents: &'static str, 10 | } 11 | 12 | impl ExternalMigration { 13 | pub fn new(sql_name: &'static str, sql_contents: &'static str) -> Self { 14 | Self { 15 | sql_name, 16 | sql_contents, 17 | } 18 | } 19 | } 20 | 21 | #[async_trait] 22 | impl Migration for ExternalMigration { 23 | fn name(&self) -> &str { 24 | self.sql_name 25 | } 26 | 27 | async fn migrate(&self, pool: &Pool) -> Result<(), Error> { 28 | let client = pool.get().await?; 29 | 30 | client.batch_execute(self.sql_contents).await?; 31 | 32 | Ok(()) 33 | } 34 | } 35 | 36 | macro_rules! external { 37 | ($a: expr) => { 38 | crate::db::migrations::external_sql::ExternalMigration::new($a, include_str!($a)) 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /front/src/style/spectre/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'variables' as *; 3 | 4 | // Dropdown 5 | .dropdown { 6 | display: inline-block; 7 | position: relative; 8 | 9 | .menu { 10 | animation: slide-down 0.15s ease 1; 11 | display: none; 12 | left: 0; 13 | max-height: 50vh; 14 | overflow-y: auto; 15 | position: absolute; 16 | top: 100%; 17 | } 18 | 19 | &.dropdown-right { 20 | .menu { 21 | left: auto; 22 | right: 0; 23 | } 24 | } 25 | 26 | &.active .menu, 27 | .dropdown-toggle:focus + .menu, 28 | .menu:hover { 29 | display: block; 30 | } 31 | 32 | // Fix dropdown-toggle border radius in button groups 33 | .btn-group { 34 | .dropdown-toggle:nth-last-child(2) { 35 | // border-bottom-right-radius: $border-radius; // old spectre.css 36 | border-bottom-right-radius: get-var('border-radius'); // old spectre.css 37 | // border-top-right-radius: $border-radius; // old spectre.css 38 | border-top-right-radius: get-var('border-radius'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```sh 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```sh 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```sh 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /src/db/feedback.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use divedb_core::FromRow; 3 | use uuid::Uuid; 4 | 5 | use crate::schema::*; 6 | 7 | use super::{DbHandle, StatementBuilder}; 8 | 9 | impl DbHandle { 10 | pub async fn add_feedback(&self, user_id: Uuid, feedback: String) -> Result { 11 | let id = Uuid::new_v4(); 12 | let client = self.pool.get().await?; 13 | let query = "insert into feedback (id, user_id, feedback) values ($1, $2, $3) returning *"; 14 | let result = client.query_one(query, &[&id, &user_id, &feedback]).await?; 15 | 16 | Feedback::from_row(result) 17 | } 18 | 19 | pub async fn get_feedback(&self, id: Option) -> Result, Error> { 20 | let mut sql = StatementBuilder::new("select * from feedback"); 21 | 22 | if let Some(ref id) = id { 23 | sql.add_param("id = ${}", id); 24 | } 25 | 26 | sql.add_sql("order by \"date\" desc"); 27 | 28 | Feedback::from_rows(self.query(sql).await?) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /front/src/style/spectre/icons/_icons-core.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | 3 | // Icon variables 4 | $icon-border-width: $border-width-lg; 5 | $icon-prefix: 'icon'; 6 | 7 | // Icon base style 8 | .#{$icon-prefix} { 9 | box-sizing: border-box; 10 | display: inline-block; 11 | font-size: inherit; 12 | font-style: normal; 13 | height: 1em; 14 | position: relative; 15 | text-indent: -9999px; 16 | vertical-align: middle; 17 | width: 1em; 18 | &::before, 19 | &::after { 20 | content: ''; 21 | display: block; 22 | left: 50%; 23 | position: absolute; 24 | top: 50%; 25 | transform: translate(-50%, -50%); 26 | } 27 | 28 | // Icon sizes 29 | &.icon-2x { 30 | font-size: 1.6rem; 31 | } 32 | 33 | &.icon-3x { 34 | font-size: 2.4rem; 35 | } 36 | 37 | &.icon-4x { 38 | font-size: 3.2rem; 39 | } 40 | } 41 | 42 | // Component icon support 43 | .accordion, 44 | .btn, 45 | .toast, 46 | .menu { 47 | .#{$icon-prefix} { 48 | vertical-align: -10%; 49 | } 50 | } 51 | 52 | .btn-lg { 53 | .#{$icon-prefix} { 54 | vertical-align: -15%; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /front/src/routes/photos/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | DiveDB - Photos 17 | 18 | 19 |
20 |
21 |
22 |

23 | Photos 24 | {#if $session.loggedIn} 25 | 26 | 27 | 28 | {/if} 29 | 30 | 31 | 32 |

33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_color.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/color' as *; 2 | @use '../variables' as *; 3 | 4 | // Background color utility mixin 5 | @mixin bg-color-variant($name: '.bg-primary', $color: 'primary-color', $hex-color: $primary-color) { 6 | #{$name} { 7 | // background: $color !important; // old spectre. 8 | background: color($color) !important; 9 | 10 | // old spectre. 11 | // @if (lightness($color) < 60) { 12 | // color: $light-color; 13 | // } 14 | @if (lightness($hex-color) < 60) { 15 | color: color('light-color'); 16 | } 17 | } 18 | } 19 | 20 | // Text color utility mixin 21 | @mixin text-color-variant($name: '.text-primary', $color: 'primary-color') { 22 | #{$name} { 23 | // color: $color !important; // old spectre. 24 | color: color($color) !important; 25 | } 26 | 27 | a#{$name} { 28 | &:focus, 29 | &:hover { 30 | // color: darken($color, 5%); // old spectre. 31 | color: color($color, $lightness: -5%); 32 | } 33 | &:visited { 34 | // color: lighten($color, 5%); 35 | color: color($color, $lightness: +5%); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /front/src/lib/icons/MastodonIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /front/src/style/spectre/_empty.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Empty states (or Blank slates) 6 | .empty { 7 | // background: $bg-color; // old spectre.css 8 | background: color('bg-color'); 9 | // border-radius: $border-radius; // old spectre.css 10 | border-radius: get-var('border-radius'); 11 | // color: $gray-color-dark; // old spectre.css 12 | color: color('gray-color-dark'); 13 | text-align: center; 14 | // padding: $unit-16 $unit-8; // old spectre.css 15 | padding: get-var('unit-16') get-var('unit-8'); 16 | 17 | .empty-icon { 18 | // margin-bottom: $layout-spacing-lg; // old spectre.css 19 | margin-bottom: get-var('layout-spacing', $suffix: 'lg', $unit: 1); 20 | } 21 | 22 | .empty-title, 23 | .empty-subtitle { 24 | // margin: $layout-spacing auto; // old spectre.css 25 | margin: get-var('layout-spacing', $unit: 1) auto; 26 | } 27 | 28 | .empty-action { 29 | // margin-top: $layout-spacing-lg; // old spectre.css 30 | margin-top: get-var('layout-spacing', $suffix: 'lg', $unit: 1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /front/src/lib/util/bounds.ts: -------------------------------------------------------------------------------- 1 | import type { LatLngBoundsExpression, LatLngLiteral } from 'leaflet'; 2 | 3 | export interface Bounds { 4 | latMin: number; 5 | lonMin: number; 6 | latMax: number; 7 | lonMax: number; 8 | } 9 | 10 | export const centerVals = (bounds: LatLngBoundsExpression): number[] => { 11 | let { latMin, lonMin, latMax, lonMax } = fromLeaflet(bounds); 12 | 13 | return [latMin + (latMax - latMin) / 2, lonMin + (lonMax - lonMin) / 2]; 14 | }; 15 | 16 | export const fromLeaflet = (bounds: LatLngBoundsExpression): Bounds => { 17 | let latMin; 18 | let lonMin; 19 | let latMax; 20 | let lonMax; 21 | 22 | if (bounds instanceof Array) { 23 | latMin = bounds[0][0]; 24 | lonMin = bounds[0][1]; 25 | latMax = bounds[1][0]; 26 | lonMax = bounds[1][1]; 27 | } else { 28 | let ne: LatLngLiteral = (bounds as any)._northEast; 29 | let sw: LatLngLiteral = (bounds as any)._southWest; 30 | 31 | latMin = sw.lat; 32 | lonMin = sw.lng; 33 | latMax = ne.lat; 34 | lonMax = ne.lng; 35 | } 36 | 37 | return { 38 | latMin, 39 | lonMin, 40 | latMax, 41 | lonMax 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /front/src/routes/divesites/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |

33 | New Dive Site 34 |

35 |
36 |
37 | 38 |
39 | -------------------------------------------------------------------------------- /src/db/migrations/V020__external_users.sql: -------------------------------------------------------------------------------- 1 | 2 | alter table users drop constraint users_username_key; 3 | 4 | alter table users alter column email drop not null; 5 | 6 | alter table users add column description text not null default ''; 7 | alter table users add column "date" timestamp with time zone not null default now(); 8 | alter table users add column photo_id uuid REFERENCES photos(id) ON DELETE SET NULL; 9 | 10 | alter table users add column external boolean not null default false; 11 | alter table users add column ap_id text; 12 | alter table users add column inbox text; 13 | 14 | create table if not exists followers( 15 | user_id uuid not null REFERENCES users(id) ON DELETE CASCADE, 16 | followed_by_id uuid not null REFERENCES users(id) ON DELETE CASCADE, 17 | "date" timestamp with time zone not null default now() 18 | ); 19 | 20 | alter table followers add primary key (user_id, followed_by_id); 21 | 22 | alter table dive_comments add column external boolean not null default false; 23 | alter table dive_comments add column ap_id text; 24 | 25 | alter table photos add column internal boolean not null default false; -------------------------------------------------------------------------------- /front/src/style/spectre/_filters.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Filters 6 | // The number of filter options 7 | $filter-number: 8 !default; 8 | 9 | %filter-checked-nav { 10 | // background: $primary-color; // old spectre.css 11 | background: color('primary-color'); 12 | // color: $light-color; // old spectre.css 13 | color: color('light-color'); 14 | } 15 | 16 | %filter-checked-body { 17 | display: none; 18 | } 19 | 20 | .filter { 21 | .filter-nav { 22 | // margin: $layout-spacing 0; // old spectre.css 23 | margin: get-var('layout-spacing', $unit: 1) 0; 24 | } 25 | 26 | .filter-body { 27 | display: flex; 28 | flex-wrap: wrap; 29 | } 30 | 31 | .filter-tag { 32 | @for $i from 0 through ($filter-number) { 33 | &#tag-#{$i}:checked ~ .filter-nav .chip[for='tag-#{$i}'] { 34 | @extend %filter-checked-nav; 35 | } 36 | } 37 | 38 | @for $i from 1 through ($filter-number) { 39 | &#tag-#{$i}:checked ~ .filter-body .filter-item:not([data-tag~='tag-#{$i}']) { 40 | @extend %filter-checked-body; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/schema/feedback.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::*; 2 | use chrono::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | use divedb_core::FromRow; 7 | 8 | use crate::graphql::SchemaContext; 9 | 10 | use super::UserInfo; 11 | 12 | #[derive(Serialize, Deserialize, Debug, Clone, FromRow)] 13 | pub struct Feedback { 14 | pub id: Uuid, 15 | pub date: DateTime, 16 | pub user_id: Uuid, 17 | pub feedback: String, 18 | } 19 | 20 | #[Object] 21 | impl Feedback { 22 | async fn id(&self) -> &Uuid { 23 | &self.id 24 | } 25 | 26 | async fn user_id(&self) -> &Uuid { 27 | &self.user_id 28 | } 29 | 30 | async fn user(&self, context: &Context<'_>) -> FieldResult { 31 | Ok(context 32 | .data::()? 33 | .web 34 | .handle 35 | .user_details(self.user_id) 36 | .await? 37 | .into()) 38 | } 39 | 40 | async fn date(&self) -> DateTime { 41 | self.date.into() 42 | } 43 | 44 | async fn feedback(&self) -> &str { 45 | &self.feedback 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /front/src/style/spectre/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Breadcrumbs 6 | .breadcrumb { 7 | list-style: none; 8 | // margin: $unit-1 0; // old spectre.css 9 | margin: get-var('unit-1') 0; 10 | // padding: $unit-1 0; // old spectre.css 11 | padding: get-var('unit-1') 0; 12 | 13 | .breadcrumb-item { 14 | // color: $gray-color-dark; // old spectre.css 15 | color: color('gray-color-dark'); 16 | display: inline-block; 17 | margin: 0; 18 | // padding: $unit-1 0; // old spectre.css 19 | padding: get-var('unit-1') 0; 20 | 21 | &:not(:last-child) { 22 | // margin-right: $unit-1; // old spectre.css 23 | margin-right: get-var('unit-1'); 24 | 25 | a { 26 | // color: $gray-color-dark; // old spectre.css 27 | color: color('gray-color-dark'); 28 | } 29 | } 30 | 31 | &:not(:first-child) { 32 | &::before { 33 | // color: $gray-color-dark; // old spectre.css 34 | color: color('gray-color-dark'); 35 | content: '/'; 36 | // padding-right: $unit-2; // old spectre.css 37 | padding-right: get-var('unit-2'); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 as builder 2 | 3 | ENV DEBIAN_FRONTEND noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y \ 7 | build-essential \ 8 | libssl-dev \ 9 | pkg-config \ 10 | curl 11 | 12 | ENV PATH=/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 13 | 14 | RUN curl https://sh.rustup.rs -sSf | \ 15 | sh -s -- -y --default-toolchain stable 16 | 17 | COPY watermark.png . 18 | COPY divedb_core divedb_core 19 | COPY divedb_macro divedb_macro 20 | 21 | COPY Cargo.toml . 22 | COPY Cargo.lock . 23 | COPY templates templates 24 | COPY src src 25 | 26 | RUN \ 27 | --mount=type=cache,target=/root/.cargo/registry \ 28 | --mount=type=cache,target=/usr/local/cargo/git \ 29 | --mount=type=cache,target=/target \ 30 | cargo build --release && cp target/release/divedb /divedb 31 | 32 | 33 | FROM ubuntu:22.04 34 | ENV DEBIAN_FRONTEND=noninteractive 35 | RUN apt-get update && apt-get install -y fontconfig fonts-ubuntu fonts-liberation libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* 36 | COPY --from=builder /divedb /usr/local/bin/divedb 37 | 38 | EXPOSE 3333 39 | 40 | CMD ["divedb"] 41 | -------------------------------------------------------------------------------- /front/src/lib/components/leaflet/Popup.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 |
44 |
45 | {@render children?.()} 46 |
47 |
48 | -------------------------------------------------------------------------------- /front/src/lib/graphql/client.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request'; 2 | import { getSdk } from './generated'; // THIS FILE IS THE GENERATED FILE 3 | import { browser } from '$app/environment'; 4 | 5 | export const graphqlClient = new GraphQLClient( 6 | browser ? `${window.location.origin}/api/graphql` : `${process.env.BACKEND_URL}/api/graphql` 7 | ); 8 | 9 | export const client = getSdk(graphqlClient, (action) => { 10 | const token = browser ? localStorage.getItem('token') : undefined; 11 | 12 | if (token == undefined) { 13 | return action(); 14 | } else { 15 | return action({ 'DiveDB-Token': token }); 16 | } 17 | }); 18 | 19 | /// For when you want to try and cache results 20 | export const graphqlGetClient = new GraphQLClient( 21 | browser ? `${window.location.origin}/api/graphql` : `${process.env.BACKEND_URL}/api/graphql`, 22 | { 23 | method: 'GET', 24 | jsonSerializer: JSON 25 | } 26 | ); 27 | 28 | export const getClient = getSdk(graphqlGetClient, (action) => { 29 | const token = browser ? localStorage.getItem('token') : undefined; 30 | 31 | if (token == undefined) { 32 | return action(); 33 | } else { 34 | return action({ 'DiveDB-Token': token }); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /front/src/style/spectre/_accordions.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'variables' as *; 3 | 4 | // Accordions 5 | .accordion { 6 | input:checked ~, 7 | &[open] { 8 | & .accordion-header > { 9 | .icon:first-child { 10 | transform: rotate(90deg); 11 | } 12 | } 13 | 14 | & .accordion-body { 15 | max-height: 50rem; 16 | } 17 | } 18 | 19 | .accordion-header { 20 | display: block; 21 | // padding: $unit-1 $unit-2; // old spectre.css 22 | padding: get-var('unit-1') get-var('unit-2'); 23 | 24 | .icon { 25 | // transition: transform 0.25s; // old spectre.css 26 | transition: transform calc(get('transition-duration') + 0.05s); // old spectre.css 27 | } 28 | } 29 | 30 | .accordion-body { 31 | // margin-bottom: $layout-spacing; // old spectre.css 32 | margin-bottom: get-var('layout-spacing', $unit: 1); 33 | max-height: 0; 34 | overflow: hidden; 35 | // transition: max-height 0.25s; // old spectre.css 36 | transition: max-height calc(get('transition-duration') + 0.05s); // old spectre.css 37 | } 38 | } 39 | 40 | // Remove default details marker in Webkit 41 | summary.accordion-header { 42 | &::-webkit-details-marker { 43 | display: none; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /front/src/routes/sealife/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |

36 | New Sealife 37 |

38 |
39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | database: 4 | image: "postgres" 5 | environment: 6 | POSTGRES_USER: divedb 7 | POSTGRES_PASSWORD: divedb 8 | POSTGRES_DB: divedb 9 | volumes: 10 | - divedb-db:/var/lib/postgresql/data 11 | ports: 12 | - "5432:5432" 13 | backend: 14 | build: . 15 | image: ghcr.io/cetra3/divedb 16 | restart: on-failure 17 | environment: 18 | CONNECT_URL: postgres://divedb:divedb@database 19 | SITE_URL: http://localhost:3333 20 | FRONTEND_URL: http://frontend:3000 21 | ADMIN_EMAIL: test@test.net 22 | SMTP_HOST: maildev 23 | SMTP_PORT: 1025 24 | SMTP_SECURITY: none 25 | ulimits: 26 | nofile: 27 | soft: 65535 28 | hard: 65535 29 | ports: 30 | - "3333:3333" 31 | depends_on: 32 | - database 33 | volumes: 34 | - ./store:/store 35 | - ./thumbs:/thumbs 36 | frontend: 37 | build: front 38 | image: ghcr.io/cetra3/divedb-front 39 | environment: 40 | BACKEND_URL: http://backend:3333 41 | maildev: 42 | image: maildev/maildev 43 | ports: 44 | - "1080:1080" 45 | - "1025:1025" 46 | 47 | volumes: 48 | divedb-db: {} 49 | -------------------------------------------------------------------------------- /front/src/routes/users/[username]/photos/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | DiveDB - Photos by @{username} 18 | 19 | 20 |
21 |
22 |
23 |

24 | Photos by @{username} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |

35 |
36 |
37 | 38 |
39 | -------------------------------------------------------------------------------- /front/src/style/spectre/mixins/_label.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/color' as *; 2 | @use '../functions/get-var' as *; 3 | @use '../variables' as *; 4 | 5 | // Label base style 6 | @mixin label-base() { 7 | // border-radius: $border-radius; // old spectre.css 8 | border-radius: get-var('border-radius'); 9 | line-height: 1.25; 10 | padding: 0.1rem 0.2rem; 11 | } 12 | 13 | // @mixin label-variant($color: $light-color, $bg-color: $primary-color) { // old spectre.css 14 | @mixin label-variant($color: 'light-color', $bg-color: 'primary-color') { 15 | // background: $bg-color; // old spectre.css 16 | background: color($bg-color); 17 | // color: $color; // old spectre.css 18 | color: color($color); 19 | } 20 | 21 | @mixin label--variant($color: color('light-color'), $bg-color: color('primary-color')) { 22 | color: $color; 23 | background: $bg-color; 24 | } 25 | 26 | /* 27 | The mixin includes an extending class of name prefixed with `label-` with the given color `$name` that includes a label variant of the given `$color` and `$bg-color`. 28 | */ 29 | @mixin label-class-variant($name: 'light', $color: 'light-color', $bg-color: 'primary-color') { 30 | &.label-#{$name} { 31 | @include label-variant($color, $bg-color); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /front/src/style/spectre/spectre.scss: -------------------------------------------------------------------------------- 1 | // Variables and mixins 2 | @forward 'variables'; 3 | @forward 'mixins'; 4 | @forward 'css-variables'; 5 | @use 'variables' as *; 6 | 7 | /*! Spectre.css v#{$version} | MIT License | github.com/picturepan2/spectre */ 8 | // Reset and dependencies 9 | @forward 'normalize'; 10 | @forward 'base'; 11 | 12 | // Elements 13 | @forward 'typography'; 14 | @forward 'asian'; 15 | @forward 'tables'; 16 | @forward 'buttons'; 17 | @forward 'forms'; 18 | @forward 'labels'; 19 | @forward 'codes'; 20 | @forward 'media'; 21 | 22 | // Layout 23 | @forward 'hero'; 24 | @forward 'layout'; 25 | @forward 'columns-order'; 26 | @forward 'navbar'; 27 | 28 | // Components 29 | @forward 'accordions'; 30 | @forward 'avatars'; 31 | @forward 'badges'; 32 | @forward 'bars'; 33 | @forward 'breadcrumbs'; 34 | @forward 'cards'; 35 | @forward 'chips'; 36 | @forward 'dropdowns'; 37 | @forward 'empty'; 38 | @forward 'menus'; 39 | @forward 'modals'; 40 | @forward 'navs'; 41 | @forward 'pagination'; 42 | @forward 'panels'; 43 | @forward 'popovers'; 44 | @forward 'steps'; 45 | @forward 'tabs'; 46 | @forward 'tiles'; 47 | @forward 'toasts'; 48 | @forward 'tooltips'; 49 | 50 | // Utility classes 51 | @forward 'animations'; 52 | @forward 'utilities'; 53 | -------------------------------------------------------------------------------- /src/db/migrations/V004__slugify_sites.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "unaccent"; 2 | 3 | CREATE OR REPLACE FUNCTION slugify("value" TEXT) 4 | RETURNS TEXT AS $$ 5 | -- removes accents (diacritic signs) from a given string -- 6 | WITH "unaccented" AS ( 7 | SELECT unaccent("value") AS "value" 8 | ), 9 | -- lowercases the string 10 | "lowercase" AS ( 11 | SELECT lower("value") AS "value" 12 | FROM "unaccented" 13 | ), 14 | -- remove single and double quotes 15 | "removed_quotes" AS ( 16 | SELECT regexp_replace("value", '[''"]+', '', 'gi') AS "value" 17 | FROM "lowercase" 18 | ), 19 | -- replaces anything that's not a letter, number, hyphen('-'), or underscore('_') with a hyphen('-') 20 | "hyphenated" AS ( 21 | SELECT regexp_replace("value", '[^a-z0-9\\-_]+', '-', 'gi') AS "value" 22 | FROM "removed_quotes" 23 | ), 24 | -- trims hyphens('-') if they exist on the head or tail of the string 25 | "trimmed" AS ( 26 | SELECT regexp_replace(regexp_replace("value", '\-+$', ''), '^\-', '') AS "value" 27 | FROM "hyphenated" 28 | ) 29 | SELECT "value" FROM "trimmed"; 30 | $$ LANGUAGE SQL STRICT IMMUTABLE; 31 | 32 | 33 | alter table dive_sites add column slug text; 34 | 35 | update dive_sites set slug = slugify(name); -------------------------------------------------------------------------------- /front/src/style/spectre/_navs.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Navs 6 | .nav { 7 | display: flex; 8 | flex-direction: column; 9 | list-style: none; 10 | // margin: $unit-1 0; // old spectre.css 11 | margin: get-var('unit-1') 0; 12 | 13 | .nav-item { 14 | a { 15 | // color: $gray-color-dark; // old spectre.css 16 | color: color('gray-color-dark'); 17 | // padding: $unit-1 $unit-2; // old spectre.css 18 | padding: get-var('unit-1') get-var('unit-2'); 19 | text-decoration: none; 20 | &:focus, 21 | &:hover { 22 | // color: $primary-color; // old spectre.css 23 | color: color('primary-color'); 24 | } 25 | } 26 | &.active { 27 | & > a { 28 | // color: darken($gray-color-dark, 10%); // old spectre.css 29 | color: color('gray-color-dark', $lightness: -10%); 30 | font-weight: bold; 31 | &:focus, 32 | &:hover { 33 | // color: $primary-color; // old spectre.css 34 | color: color('primary-color'); 35 | } 36 | } 37 | } 38 | } 39 | 40 | & .nav { 41 | // margin-bottom: $unit-2; // old spectre.css 42 | margin-bottom: get-var('unit-2'); 43 | // margin-left: $unit-4; // old spectre.css 44 | margin-left: get-var('unit-4'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/db/migrations/V006__sealife.sql: -------------------------------------------------------------------------------- 1 | 2 | create table if not exists sealife ( 3 | id uuid primary key, 4 | name text not null, 5 | scientific_name text, 6 | description text not null, 7 | photo_id uuid REFERENCES photos(id) ON DELETE SET NULL, 8 | "date" timestamp with time zone not null, 9 | slug text not null 10 | ); 11 | 12 | create table if not exists sealife_tags ( 13 | sealife_id uuid not null REFERENCES sealife(id) ON DELETE CASCADE, 14 | photo_id uuid not null REFERENCES photos(id) ON DELETE CASCADE, 15 | 16 | u_min double precision not null, 17 | v_min double precision not null, 18 | u_max double precision not null, 19 | v_max double precision not null 20 | ); 21 | 22 | create table if not exists categories ( 23 | id uuid primary key, 24 | name text not null 25 | ); 26 | 27 | create table if not exists category_values ( 28 | id uuid primary key, 29 | category_id uuid not null REFERENCES categories(id) ON DELETE CASCADE, 30 | value text not null 31 | ); 32 | 33 | create table if not exists sealife_category_values ( 34 | id uuid primary key, 35 | sealife_id uuid not null REFERENCES sealife(id) ON DELETE CASCADE, 36 | category_value_id uuid not null REFERENCES categories(id) ON DELETE CASCADE 37 | ); 38 | -------------------------------------------------------------------------------- /front/src/routes/dives/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { DiveWithMetricsFragment } from '$lib/graphql/generated'; 3 | import type { PageLoad } from './$types'; 4 | import { unified } from 'unified'; 5 | import remarkParse from 'remark-parse'; 6 | import remarkRehype from 'remark-rehype'; 7 | import rehypeSanitize from 'rehype-sanitize'; 8 | import rehypeStringify from 'rehype-stringify'; 9 | 10 | export const load: PageLoad = async ({ params }) => { 11 | try { 12 | let response = await client.getDive({ id: params.id }); 13 | 14 | let dive = response.dives[0]; 15 | 16 | let relatedDives: DiveWithMetricsFragment[] = []; 17 | 18 | let mdDesc = undefined; 19 | 20 | if (dive && dive.diveSiteId) { 21 | const mdProc = unified() 22 | .use(remarkParse) 23 | .use(remarkRehype) 24 | .use(rehypeSanitize) 25 | .use(rehypeStringify); 26 | 27 | mdDesc = await mdProc.process(dive.description); 28 | 29 | let relatedResponse = await client.getDives({ 30 | diveSite: dive.diveSiteId 31 | }); 32 | 33 | relatedDives = relatedResponse.dives.filter((val) => val.id != dive.id); 34 | } 35 | 36 | return { 37 | dive, 38 | relatedDives, 39 | mdDesc, 40 | siteUrl: response.siteUrl 41 | }; 42 | } catch (error) { 43 | return {}; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /front/src/style/spectre/utilities/_text.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/text' as *; 2 | @use '../variables' as *; 3 | 4 | // Text 5 | // Text alignment utilities 6 | .text-left { 7 | text-align: left; 8 | } 9 | 10 | .text-right { 11 | text-align: right; 12 | } 13 | 14 | .text-center { 15 | text-align: center; 16 | } 17 | 18 | .text-justify { 19 | text-align: justify; 20 | } 21 | 22 | // Text transform utilities 23 | .text-lowercase { 24 | text-transform: lowercase; 25 | } 26 | 27 | .text-uppercase { 28 | text-transform: uppercase; 29 | } 30 | 31 | .text-capitalize { 32 | text-transform: capitalize; 33 | } 34 | 35 | // Text style utilities 36 | .text-normal { 37 | font-weight: normal; 38 | } 39 | 40 | .text-bold { 41 | font-weight: bold; 42 | } 43 | 44 | .text-italic { 45 | font-style: italic; 46 | } 47 | 48 | .text-large { 49 | font-size: 1.2em; 50 | } 51 | 52 | .text-small { 53 | font-size: 0.9em; 54 | } 55 | 56 | .text-tiny { 57 | font-size: 0.8em; 58 | } 59 | 60 | .text-muted { 61 | opacity: 0.8; 62 | } 63 | 64 | // Text overflow utilities 65 | .text-ellipsis { 66 | @include text-ellipsis(); 67 | } 68 | 69 | .text-clip { 70 | overflow: hidden; 71 | text-overflow: clip; 72 | white-space: nowrap; 73 | } 74 | 75 | .text-break { 76 | hyphens: auto; 77 | word-break: break-word; 78 | word-wrap: break-word; 79 | } 80 | -------------------------------------------------------------------------------- /front/src/lib/components/leaflet/TileLayer.svelte: -------------------------------------------------------------------------------- 1 | 55 | -------------------------------------------------------------------------------- /front/src/routes/photos/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { client } from '$lib/graphql/client'; 2 | import type { GetPhotosQueryVariables, PhotoSummaryFragment } from '$lib/graphql/generated'; 3 | import type { PageLoad } from './$types'; 4 | 5 | export const load: PageLoad = async ({ params }) => { 6 | try { 7 | let response = await client.getPhotos({ id: params.id }); 8 | 9 | let photo = response.photos[0]; 10 | 11 | let query: GetPhotosQueryVariables | undefined = undefined; 12 | let relatedTitle: string | undefined = undefined; 13 | let relatedPhotos: PhotoSummaryFragment[] = []; 14 | if (photo) { 15 | if (photo.sealife?.id) { 16 | query = { sealifeId: photo.sealife?.id }; 17 | relatedTitle = `of ${photo.sealife.name}`; 18 | } else if (photo.diveSite?.id) { 19 | query = { diveSite: photo.diveSite?.id }; 20 | relatedTitle = `of ${photo.diveSite.name}`; 21 | } else { 22 | query = { userId: photo.userId }; 23 | relatedTitle = `by the photographer`; 24 | } 25 | 26 | let relatedResponse = await client.getPhotos(query); 27 | 28 | relatedPhotos = relatedResponse.photos; 29 | } 30 | 31 | let siteUrl = response.siteUrl; 32 | 33 | return { 34 | photo, 35 | query, 36 | relatedPhotos, 37 | relatedTitle, 38 | siteUrl 39 | }; 40 | } catch (error) { 41 | return {}; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /front/src/style/spectre/_codes.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'mixins/label' as *; 4 | @use 'variables' as *; 5 | 6 | // Codes 7 | code { 8 | @include label-base(); 9 | // @include label-variant($code-color, lighten($code-color, 42.5%)); // old spectre.css 10 | @include label--variant(color('code-color'), color('code-color', $lightness: +42.5%)); 11 | font-size: 85%; 12 | } 13 | 14 | .code { 15 | // border-radius: $border-radius; // old spectre.css 16 | border-radius: get-var('border-radius'); 17 | // color: $body-font-color; // old spectre.css 18 | color: color('body-font-color'); 19 | position: relative; 20 | 21 | &::before { 22 | // color: $gray-color; // old spectre.css 23 | color: color('gray-color'); 24 | content: attr(data-lang); 25 | // font-size: $font-size-sm; // old spectre.css 26 | font-size: get-var('font-size', $suffix: 'sm'); 27 | position: absolute; 28 | // right: $layout-spacing; // old spectre.css 29 | right: get-var('layout-spacing', $unit: 1); 30 | // top: $unit-h; // old spectre.css 31 | top: get-var('unit-h'); 32 | } 33 | 34 | code { 35 | // background: $bg-color; // old spectre.css 36 | background: color('bg-color'); 37 | color: inherit; 38 | display: block; 39 | line-height: 1.5; 40 | overflow-x: auto; 41 | padding: 1rem; 42 | width: 100%; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /front/src/routes/dives/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | DiveDB - Dives 17 | 18 |
19 |
20 |
21 |

22 | 23 | Dives 24 | 25 | {#if $session.loggedIn} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {/if} 37 |

38 |
39 |
40 | {#if dives !== undefined} 41 | 42 | {:else} 43 |
44 |
45 |
46 | {/if} 47 |
48 | -------------------------------------------------------------------------------- /src/db/password_reset.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use divedb_core::FromRow; 3 | use uuid::Uuid; 4 | 5 | use crate::schema::*; 6 | 7 | use super::{DbHandle, StatementBuilder}; 8 | 9 | impl DbHandle { 10 | pub async fn request_reset(&self, user_id: Uuid) -> Result { 11 | let id = Uuid::new_v4(); 12 | let client = self.pool.get().await?; 13 | let query = "insert into password_reset (id, user_id) values ($1, $2) returning *"; 14 | let result = client.query_one(query, &[&id, &user_id]).await?; 15 | 16 | PasswordReset::from_row(result) 17 | } 18 | 19 | pub async fn get_valid_resets(&self, user_id: Uuid) -> Result, Error> { 20 | let mut sql = StatementBuilder::new("select id, user_id, \"date\" from password_reset"); 21 | 22 | sql.add_param("user_id = ${}", &user_id); 23 | 24 | sql.add_sql(" AND \"date\" > now() - interval '24 hours' "); 25 | 26 | PasswordReset::from_rows(self.query(sql).await?) 27 | } 28 | 29 | pub async fn remove_reset(&self, id: Uuid) -> Result<(), Error> { 30 | let client = self.pool.get().await?; 31 | 32 | let sql = 33 | "delete from password_reset where \"date\" > now() - interval '24 hours' OR id = $1"; 34 | 35 | client.execute(sql, &[&id]).await?; 36 | 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /templates/password_reset.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Password Reset 11 | 12 | 13 | We have received a request to reset your password for DiveDB 14 | 15 | 16 | To reset your password, click the button below 17 | 18 | 20 | Reset Password 21 | 22 | 23 | If you did not make this request, feel free to ignore this email 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /front/src/lib/components/EditableMap.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | 34 | 35 | 36 | 37 |
38 | 39 | 49 | -------------------------------------------------------------------------------- /front/src/lib/components/DiveSiteSummary.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | 29 |
30 |
{site.name}
31 |
32 | 33 |
34 |
35 |
{site.summary}
36 | 41 |
42 |
43 | -------------------------------------------------------------------------------- /front/src/lib/components/SealifeSummary.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | 28 |
29 |
{sealife.name}
30 | {#if sealife.scientificName} 31 |
32 | {sealife.scientificName} 33 |
34 | {/if} 35 |
36 |
{sealife.summary}
37 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /templates/email_verification.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Email Verification 11 | 12 | 13 | Welcome to DiveDB! Please verify your email address to complete your registration. 14 | 15 | 16 | To verify your email address, click the button below: 17 | 18 | 20 | Verify Email 21 | 22 | 23 | If you did not create an account with DiveDB, please ignore this email. 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /front/src/lib/components/EditableRegion.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 | 31 | 32 | 33 | 34 |
35 | 36 | 46 | -------------------------------------------------------------------------------- /front/src/routes/facebook/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
35 |
36 | {#if errors} 37 |
{errors}
38 | {/if} 39 | {#if loading} 40 |
41 |
42 |
43 | {/if} 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/schema/categories.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use async_graphql::*; 4 | use divedb_core::FromRow; 5 | use serde::{Deserialize, Serialize}; 6 | use uuid::Uuid; 7 | 8 | use crate::graphql::SchemaContext; 9 | 10 | #[derive(Serialize, Deserialize, Debug, Clone, FromRow)] 11 | pub struct Category { 12 | pub id: Uuid, 13 | pub name: String, 14 | } 15 | 16 | #[Object] 17 | impl Category { 18 | async fn id(&self) -> &Uuid { 19 | &self.id 20 | } 21 | 22 | async fn name(&self) -> &str { 23 | &self.name 24 | } 25 | 26 | async fn values(&self, context: &Context<'_>) -> FieldResult> { 27 | let context = context.data::()?; 28 | 29 | Ok(context.web.handle.category_values(Some(self.id)).await?) 30 | } 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Debug, Clone, SimpleObject, FromRow)] 34 | pub struct CategoryValue { 35 | pub id: Uuid, 36 | pub category_id: Uuid, 37 | pub value: String, 38 | } 39 | 40 | pub type CategoryMap = HashMap>; 41 | 42 | #[derive(Serialize, Deserialize, Debug, Clone, InputObject)] 43 | pub struct CreateCategory { 44 | pub id: Option, 45 | pub name: String, 46 | } 47 | 48 | #[derive(Serialize, Deserialize, Debug, Clone, InputObject)] 49 | pub struct CreateCategoryValue { 50 | pub id: Option, 51 | pub category_id: Uuid, 52 | pub value: String, 53 | } 54 | -------------------------------------------------------------------------------- /front/src/style/spectre/_autocomplete.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'mixins/shadow' as *; 4 | @use 'variables' as *; 5 | 6 | // Autocomplete 7 | .form-autocomplete { 8 | position: relative; 9 | 10 | .form-autocomplete-input { 11 | align-content: flex-start; 12 | display: flex; 13 | flex-wrap: wrap; 14 | height: auto; 15 | // min-height: $unit-8; // old spectre.css 16 | min-height: get-var('unit-8'); 17 | // padding: $unit-h; // old spectre.css 18 | padding: get-var('unit-h'); 19 | 20 | &.is-focused { 21 | @include control-shadow(); 22 | // border-color: $primary-color; // old spectre.css 23 | border-color: color('primary-color'); 24 | } 25 | 26 | .form-input { 27 | border-color: transparent; 28 | box-shadow: none; 29 | display: inline-block; 30 | flex: 1 0 auto; 31 | // height: $unit-6; // old spectre.css 32 | height: get-var('unit-6'); 33 | // line-height: $unit-4; // old spectre.css 34 | line-height: get-var('unit-4'); 35 | // margin: $unit-h; // old spectre.css 36 | margin: get-var('unit-h'); 37 | width: auto; 38 | } 39 | } 40 | 41 | .menu { 42 | left: 0; 43 | position: absolute; 44 | top: 100%; 45 | width: 100%; 46 | } 47 | 48 | &.autocomplete-oneline { 49 | .form-autocomplete-input { 50 | flex-wrap: nowrap; 51 | overflow-x: auto; 52 | } 53 | 54 | .chip { 55 | flex: 1 0 auto; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_core-colors.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/define-color-based-on' as *; 2 | @use '../mixins/define-color' as *; 3 | @use '../variables' as *; 4 | 5 | /* 6 | Spectre.css core colors. Alphabetical order. 7 | */ 8 | :root, 9 | :host { 10 | // Accent. 11 | @include define-color('accent-color', $accent-color); // #9932CC 12 | 13 | // Dark. 14 | @include define-color('dark-color', $dark-color); // #303742 15 | 16 | // Light. 17 | @include define-color('light-color', $light-color); // #ffffff 18 | 19 | // Primary. 20 | @include define-color('primary-color', $primary-color); // #5755d9 21 | @include define-color-based-on( 22 | 'primary-color-dark', 23 | 'primary-color', 24 | $lightness: -3% 25 | ); // darken($primary-color, 3%) 26 | @include define-color-based-on( 27 | 'primary-color-light', 28 | 'primary-color', 29 | $lightness: +3% 30 | ); // lighten($primary-color, 3%) 31 | 32 | // Secondary. 33 | @include define-color-based-on( 34 | 'secondary-color', 35 | 'primary-color', 36 | $lightness: +37.5% 37 | ); // lighten($primary-color, 37.5%) !default; 38 | @include define-color-based-on( 39 | 'secondary-color-dark', 40 | 'secondary-color', 41 | $lightness: -3% 42 | ); // darken($secondary-color, 3%) !default; 43 | @include define-color-based-on( 44 | 'secondary-color-light', 45 | 'secondary-color', 46 | $lightness: +3% 47 | ); // lighten($secondary-color, 3%) !default; 48 | } 49 | -------------------------------------------------------------------------------- /front/src/style/spectre/_base.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'mixins/shadow' as *; 4 | @use 'variables' as *; 5 | 6 | // Base 7 | *, 8 | *::before, 9 | *::after { 10 | box-sizing: inherit; 11 | } 12 | 13 | html { 14 | box-sizing: border-box; 15 | // font-size: $html-font-size; // old spectre.css 16 | font-size: get-var('html-font-size'); 17 | // line-height: $html-line-height; // old spectre.css 18 | line-height: get-var('html-line-height'); 19 | -webkit-tap-highlight-color: transparent; 20 | } 21 | 22 | body { 23 | // background: $body-bg; // old spectre.css 24 | background: color('body-bg-color'); 25 | // color: $body-font-color; // old spectre.css 26 | color: color('body-font-color'); 27 | font-family: $body-font-family; 28 | // font-size: $font-size; // old spectre.css 29 | font-size: get-var('font-size'); 30 | overflow-x: hidden; 31 | text-rendering: optimizeLegibility; 32 | } 33 | 34 | a { 35 | // color: $link-color; // old spectre.css 36 | color: color('link-color'); 37 | outline: none; 38 | text-decoration: none; 39 | 40 | &:focus { 41 | @include control-shadow(); 42 | } 43 | 44 | &:focus, 45 | &:hover, 46 | &:active, 47 | &.active { 48 | // color: $link-color-dark; // old spectre.css 49 | color: color('link-color-dark'); 50 | text-decoration: underline; 51 | } 52 | 53 | &:visited { 54 | // color: $link-color-light; // old spectre.css 55 | color: color('link-color-light'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /front/src/lib/components/dives/DiveLabels.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {#if dive.diveSite} 35 | 36 | {dive.diveSite.name} 37 | 38 | {/if} 39 | {#if dive.date} 40 | {new Date(dive.date).toLocaleDateString()} 41 | {/if} 42 |
43 | 44 | 53 | -------------------------------------------------------------------------------- /front/src/lib/components/dives/DiveList.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 | 47 |
48 | {#each dives as dive} 49 | 50 | {/each} 51 |
52 | -------------------------------------------------------------------------------- /front/src/routes/admin/region/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |

Add Region

35 |
36 |
37 | 38 |
39 | {#if errors} 40 |
41 |
{errors}
42 |
43 | {/if} 44 | {#if loading} 45 |
46 |
47 |
48 | {/if} 49 |
50 |
51 | -------------------------------------------------------------------------------- /front/src/style/spectre/_chips.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'functions/var-negative' as *; 4 | @use 'variables' as *; 5 | 6 | // Chips 7 | .chip { 8 | align-items: center; 9 | // background: $bg-color-dark; // old spectre.css 10 | background: color('bg-color-dark'); 11 | border-radius: 5rem; 12 | display: inline-flex; 13 | font-size: 90%; 14 | // height: $unit-6; // old spectre.css 15 | height: get-var('unit-6'); 16 | // line-height: $unit-4; // old spectre.css 17 | line-height: get-var('unit-4'); 18 | // margin: $unit-h; // old spectre.css 19 | margin: get-var('unit-h'); 20 | // max-width: $control-width-sm; // old spectre.css 21 | max-width: get-var('control-width', $suffix: 'sm'); 22 | overflow: hidden; 23 | // padding: $unit-1 $unit-2; // old spectre.css 24 | padding: get-var('unit-1') get-var('unit-2'); 25 | text-decoration: none; 26 | text-overflow: ellipsis; 27 | vertical-align: middle; 28 | white-space: nowrap; 29 | 30 | &.active { 31 | // background: $primary-color; // old spectre.css 32 | background: color('primary-color'); 33 | // color: $light-color; // old spectre.css 34 | color: color('light-color'); 35 | } 36 | 37 | .avatar { 38 | // margin-left: -$unit-2; // old spectre.css 39 | margin-left: var-negative(get-var('unit-2')); 40 | // margin-right: $unit-1; // old spectre.css 41 | margin-right: get-var('unit-1'); 42 | } 43 | 44 | .btn-clear { 45 | border-radius: 50%; 46 | transform: scale(0.75); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /front/src/lib/components/leaflet/LeafletMap.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |
61 | {#if map} 62 | {@render children?.()} 63 | {/if} 64 |
65 | -------------------------------------------------------------------------------- /front/src/routes/users/[username]/dives/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | DiveDB - Dives by @{username} 18 | 19 |
20 |
21 |
22 |

23 | 24 | Dives by @{username} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |

40 |
41 |
42 | {#if dives !== undefined} 43 | 44 | {:else} 45 |
46 |
47 |
48 | {/if} 49 |
50 | -------------------------------------------------------------------------------- /front/src/style/spectre/utilities/_position.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins/clearfix' as *; 2 | @use '../mixins/position' as *; 3 | @use '../variables' as *; 4 | 5 | // Position 6 | .clearfix { 7 | @include clearfix(); 8 | } 9 | 10 | .float-left { 11 | float: left !important; 12 | } 13 | 14 | .float-right { 15 | float: right !important; 16 | } 17 | 18 | .p-relative { 19 | position: relative !important; 20 | } 21 | 22 | .p-absolute { 23 | position: absolute !important; 24 | } 25 | 26 | .p-fixed { 27 | position: fixed !important; 28 | } 29 | 30 | .p-sticky { 31 | position: sticky !important; 32 | } 33 | 34 | .p-centered { 35 | display: block; 36 | float: none; 37 | margin-left: auto; 38 | margin-right: auto; 39 | } 40 | 41 | .flex-centered { 42 | align-items: center; 43 | display: flex; 44 | justify-content: center; 45 | } 46 | 47 | // Spacing 48 | // @include margin-variant(0, 0); // old spectre.css 49 | @include margin-variant(0, 'unit-0'); 50 | 51 | // @include margin-variant(1, $unit-1); // old spectre.css 52 | @include margin-variant(1, 'unit-1'); 53 | 54 | // @include margin-variant(2, $unit-2); // old spectre.css 55 | @include margin-variant(2, 'unit-2'); 56 | 57 | // @include padding-variant(0, 0); // old spectre.css 58 | @include padding-variant(0, 'unit-0'); // old spectre.css 59 | 60 | // @include padding-variant(1, $unit-1); // old spectre.css 61 | @include padding-variant(1, 'unit-1'); 62 | 63 | // @include padding-variant(2, $unit-2); // old spectre.css 64 | @include padding-variant(2, 'unit-2'); 65 | -------------------------------------------------------------------------------- /front/src/style/spectre/css-variables/_control-padding.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/get-var' as *; 2 | @use '../functions/strip-unit' as *; 3 | @use '../mixins/set-var' as *; 4 | @use '../variables' as *; 5 | 6 | /* 7 | Control padding. Alphabetical order. 8 | */ 9 | :root, 10 | :host { 11 | // padding-x 12 | @include set-var('control-padding-x', $control-padding-x); // $unit-2 13 | // padding-x-lg 14 | @include set-var( 15 | 'control-padding-x', 16 | calc(get-var('control-padding-x') * 1.5), 17 | $suffix: 'lg' 18 | ); // $unit-2 * 1.5 19 | // padding-x-sm 20 | @include set-var( 21 | 'control-padding-x', 22 | calc(get-var('control-padding-x') * 0.75), 23 | $suffix: 'sm' 24 | ); // $unit-2 * 0.75 25 | 26 | // padding-y 27 | @include set-var( 28 | 'control-padding-y', 29 | calc((get-var('control-size') - get-var('line-height')) * 0.5 - get-var('border-width')) 30 | ); // ($control-size - $line-height) * 0.5 - $border-width 31 | // padding-y-lg 32 | @include set-var( 33 | 'control-padding-y', 34 | calc( 35 | (get-var('control-size', $suffix: 'lg') - get-var('line-height')) * 36 | 0.5 - get-var('border-width') 37 | ), 38 | $suffix: 'lg' 39 | ); // ($control-size-lg - $line-height) * 0.5 - $border-width 40 | // padding-y-sm 41 | @include set-var( 42 | 'control-padding-y', 43 | calc( 44 | (get-var('control-size', $suffix: 'sm') - get-var('line-height')) * 45 | 0.5 - get-var('border-width') 46 | ), 47 | $suffix: 'sm' 48 | ); // ($control-size-sm - $line-height) * 0.5 - $border-width 49 | } 50 | -------------------------------------------------------------------------------- /front/src/style/spectre/_tables.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Tables 6 | .table { 7 | border-collapse: collapse; 8 | border-spacing: 0; 9 | width: 100%; 10 | @if $rtl == true { 11 | text-align: right; 12 | } @else { 13 | text-align: left; 14 | } 15 | 16 | &.table-striped { 17 | tbody { 18 | tr:nth-of-type(odd) { 19 | // background: $bg-color; // old spectre.css 20 | background: color('bg-color'); 21 | } 22 | } 23 | } 24 | 25 | &, 26 | &.table-striped { 27 | tbody { 28 | tr { 29 | &.active { 30 | // background: $bg-color-dark; // old spectre.css 31 | background: color('bg-color-dark'); 32 | } 33 | } 34 | } 35 | } 36 | 37 | &.table-hover { 38 | tbody { 39 | tr { 40 | &:hover { 41 | // background: $bg-color-dark; // old spectre.css 42 | background: color('bg-color-dark'); 43 | } 44 | } 45 | } 46 | } 47 | 48 | // Scollable tables 49 | &.table-scroll { 50 | display: block; 51 | overflow-x: auto; 52 | padding-bottom: 0.75rem; 53 | white-space: nowrap; 54 | } 55 | 56 | td, 57 | th { 58 | // border-bottom: $border-width solid $border-color; // old spectre.css 59 | border-bottom: get-var('border-width') solid color('border-color'); 60 | // padding: $unit-3 $unit-2; // old spectre.css 61 | padding: get-var('unit-3') get-var('unit-2'); 62 | } 63 | th { 64 | // border-bottom-width: $border-width-lg; // old spectre.css 65 | border-bottom-width: get-var('border-width', $suffix: 'lg'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /divedb_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, DeriveInput, Fields}; 4 | 5 | /// This derives the `FromRow` trait for structs 6 | /// Requires that the query is in field order, as it just uses row indices 7 | #[proc_macro_derive(FromRow)] 8 | pub fn derive_from_row(input: TokenStream) -> TokenStream { 9 | // Parse it as a proc macro 10 | let input = parse_macro_input!(input as DeriveInput); 11 | 12 | if let syn::Data::Struct(ref data) = input.data { 13 | if let Fields::Named(ref fields) = data.fields { 14 | // Collect up each field and index, and return it. 15 | let field_vals = fields.named.iter().enumerate().map(|(i, field)| { 16 | let name = &field.ident; 17 | quote!(#name: row.try_get(#i)?) 18 | }); 19 | 20 | let name = input.ident; 21 | 22 | return TokenStream::from(quote!( 23 | impl ::divedb_core::FromRow for #name { 24 | fn from_row(row: ::tokio_postgres::Row) -> Result { 25 | Ok(Self { 26 | #(#field_vals),* 27 | }) 28 | } 29 | })); 30 | } 31 | } 32 | 33 | // We don't care about any other variants, so emit an error here 34 | TokenStream::from( 35 | syn::Error::new( 36 | input.ident.span(), 37 | "Only structs with named fields can derive `FromRow`", 38 | ) 39 | .to_compile_error(), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/db/email_verification.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use divedb_core::FromRow; 3 | use uuid::Uuid; 4 | 5 | use crate::schema::*; 6 | 7 | use super::{DbHandle, StatementBuilder}; 8 | 9 | impl DbHandle { 10 | pub async fn request_email_verification( 11 | &self, 12 | user_id: Uuid, 13 | ) -> Result { 14 | let id = Uuid::new_v4(); 15 | let client = self.pool.get().await?; 16 | let query = "insert into email_verification (id, user_id) values ($1, $2) returning *"; 17 | let result = client.query_one(query, &[&id, &user_id]).await?; 18 | 19 | EmailVerification::from_row(result) 20 | } 21 | 22 | pub async fn get_valid_email_verifications( 23 | &self, 24 | user_id: Uuid, 25 | ) -> Result, Error> { 26 | let mut sql = StatementBuilder::new("select id, user_id, \"date\" from email_verification"); 27 | 28 | sql.add_param("user_id = ${}", &user_id); 29 | 30 | sql.add_sql(" AND \"date\" > now() - interval '24 hours' "); 31 | 32 | EmailVerification::from_rows(self.query(sql).await?) 33 | } 34 | 35 | pub async fn mark_email_verified(&self, user_id: Uuid) -> Result<(), Error> { 36 | let client = self.pool.get().await?; 37 | 38 | let sql = "update users set email_verified = true where id = $1"; 39 | client.execute(sql, &[&user_id]).await?; 40 | 41 | let sql = "delete from email_verification where user_id = $1"; 42 | client.execute(sql, &[&user_id]).await?; 43 | 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /front/src/lib/components/SearchResult.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 | 35 | 36 |
37 |
{result.name}
38 | {#if result.scientificName} 39 |
40 | {result.scientificName} 41 |
42 | {/if} 43 |
44 |
{result.summary}
45 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /front/src/lib/components/forms/LikeHeart.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | {#if $session.loggedIn || likes > 0} 35 | 42 | {#if liked} 43 | 44 | {:else} 45 | 46 | {/if} 47 | {likes > 0 ? likes : ''} 48 | 49 | {/if} 50 | 51 | 67 | -------------------------------------------------------------------------------- /front/src/lib/components/forms/EditPhoto.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /front/src/lib/components/leaflet/DrawRectangle.svelte: -------------------------------------------------------------------------------- 1 | 64 | -------------------------------------------------------------------------------- /front/src/routes/divesites/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | DiveDB - Dive Sites 19 | 20 | 21 |
22 |
23 |
24 |

25 | Dive Sites 26 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |

41 |
42 |
43 |
44 | {#if showSearch} 45 | 46 | {/if} 47 | 48 | {#if !showSearch || query == undefined || query == ''} 49 |
50 |
51 | {#each diveSites.diveSites as site} 52 | 53 | {/each} 54 |
55 |
56 | {/if} 57 | -------------------------------------------------------------------------------- /front/src/lib/components/forms/Markdown.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 80 | -------------------------------------------------------------------------------- /front/src/style/spectre/_badges.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Badges 6 | .badge { 7 | position: relative; 8 | white-space: nowrap; 9 | 10 | &[data-badge], 11 | &:not([data-badge]) { 12 | &::after { 13 | // background: $primary-color; // old spectre.css 14 | background: color('primary-color'); 15 | background-clip: padding-box; 16 | border-radius: 0.5rem; 17 | // box-shadow: 0 0 0 .1rem $bg-color-light; // old spectre.css 18 | box-shadow: 0 0 0 0.1rem color('bg-color-light'); 19 | // color: $light-color; // old spectre.css 20 | color: color('light-color'); 21 | content: attr(data-badge); 22 | display: inline-block; 23 | transform: translate(-0.05rem, -0.5rem); 24 | } 25 | } 26 | &[data-badge] { 27 | &::after { 28 | // font-size: $font-size-sm; // old spectre.css 29 | font-size: get-var('font-size', $suffix: 'sm'); 30 | height: 0.9rem; 31 | line-height: 1; 32 | min-width: 0.9rem; 33 | padding: 0.1rem 0.2rem; 34 | text-align: center; 35 | white-space: nowrap; 36 | } 37 | } 38 | &:not([data-badge]), 39 | &[data-badge=''] { 40 | &::after { 41 | height: 6px; 42 | min-width: 6px; 43 | padding: 0; 44 | width: 6px; 45 | } 46 | } 47 | 48 | // Badges for Buttons 49 | &.btn { 50 | &::after { 51 | position: absolute; 52 | top: 0; 53 | right: 0; 54 | transform: translate(50%, -50%); 55 | } 56 | } 57 | 58 | // Badges for Avatars 59 | &.avatar { 60 | &::after { 61 | position: absolute; 62 | top: 14.64%; 63 | right: 14.64%; 64 | transform: translate(50%, -50%); 65 | // z-index: $zindex-1; // old spectre.css 66 | z-index: get-var('z-index-1'); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /front/src/lib/components/forms/VerifyEmail.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 |
32 |

Verify Email

33 |
34 |
35 | Your email address hasn't been verified. Please check your email for a link to verify it. If you 36 | haven't received the email verification yet, please press the button below. 37 |
38 |
39 |
40 |
41 |
42 | 45 |
46 |
47 |
48 | {#if saved} 49 |
Email Verification has been resent!
50 | {/if} 51 | {#if errors} 52 |
{errors}
53 | {/if} 54 | {#if loading} 55 |
56 | {/if} 57 |
58 |
59 | -------------------------------------------------------------------------------- /front/src/style/spectre/_media.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'variables' as *; 3 | 4 | // Media 5 | // Image responsive 6 | .img-responsive { 7 | display: block; 8 | height: auto; 9 | max-width: 100%; 10 | } 11 | 12 | // object-fit support is coming to Microsoft Edge 13 | // https://developer.microsoft.com/en-us/microsoft-edge/platform/status/objectfitandobjectposition/ 14 | .img-fit-cover { 15 | object-fit: cover; 16 | } 17 | 18 | .img-fit-contain { 19 | object-fit: contain; 20 | } 21 | 22 | // Video responsive 23 | .video-responsive { 24 | display: block; 25 | overflow: hidden; 26 | padding: 0; 27 | position: relative; 28 | width: 100%; 29 | &::before { 30 | content: ''; 31 | display: block; 32 | padding-bottom: 56.25%; // Default ratio 16:9, you can calculate this value by dividing 9 by 16 33 | } 34 | 35 | iframe, 36 | object, 37 | embed { 38 | border: 0; 39 | bottom: 0; 40 | height: 100%; 41 | left: 0; 42 | position: absolute; 43 | right: 0; 44 | top: 0; 45 | width: 100%; 46 | } 47 | } 48 | 49 | video.video-responsive { 50 | height: auto; 51 | max-width: 100%; 52 | 53 | &::before { 54 | content: none; 55 | } 56 | } 57 | 58 | .video-responsive-4-3 { 59 | &::before { 60 | padding-bottom: 75%; // Ratio 4:3 61 | } 62 | } 63 | 64 | .video-responsive-1-1 { 65 | &::before { 66 | padding-bottom: 100%; // Ratio 1:1 67 | } 68 | } 69 | 70 | // Figure 71 | .figure { 72 | // margin: 0 0 $layout-spacing 0; // old spectre.css 73 | margin: 0 0 get-var('layout-spacing', $unit: 1) 0; 74 | 75 | .figure-caption { 76 | // color: $gray-color-dark; // old spectre.css 77 | color: $gray-color-dark; 78 | // margin-top: $layout-spacing; // old spectre.css 79 | margin-top: get-var('layout-spacing', $unit: 1); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /front/src/style/spectre/_pagination.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'variables' as *; 4 | 5 | // Pagination 6 | .pagination { 7 | display: flex; 8 | list-style: none; 9 | // margin: $unit-1 0; // old spectre.css 10 | margin: get-var('unit-1') 0; 11 | // padding: $unit-1 0; // old spectre.css 12 | padding: get-var('unit-1') 0; 13 | 14 | .page-item { 15 | // margin: $unit-1 $unit-o; // old spectre.css 16 | margin: get-var('unit-1') get-var('unit-o'); 17 | 18 | span { 19 | display: inline-block; 20 | // padding: $unit-1 $unit-1; // old spectre.css 21 | padding: get-var('unit-1') get-var('unit-1'); 22 | } 23 | 24 | a { 25 | // border-radius: $border-radius; // old spectre.css 26 | border-radius: get-var('border-radius'); 27 | display: inline-block; 28 | // padding: $unit-1 $unit-2; // old spectre.css 29 | padding: get-var('unit-1') get-var('unit-2'); 30 | text-decoration: none; 31 | &:focus, 32 | &:hover { 33 | // color: $primary-color; // old spectre.css 34 | color: color('primary-color'); 35 | } 36 | } 37 | 38 | &.disabled { 39 | a { 40 | cursor: default; 41 | opacity: 0.5; 42 | pointer-events: none; 43 | } 44 | } 45 | 46 | &.active { 47 | a { 48 | // background: $primary-color; // old spectre.css 49 | background: color('primary-color'); 50 | // color: $light-color; // old spectre.css 51 | color: color('light-color'); 52 | } 53 | } 54 | 55 | &.page-prev, 56 | &.page-next { 57 | flex: 1 0 50%; 58 | } 59 | 60 | &.page-next { 61 | text-align: right; 62 | } 63 | 64 | .page-item-title { 65 | margin: 0; 66 | } 67 | 68 | .page-item-subtitle { 69 | margin: 0; 70 | opacity: 0.5; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /front/src/lib/components/dives/DiveSummary.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |
21 | 22 |
23 | 28 |
29 |
30 | Max Depth: {dive.depth.toFixed(2)}m, Duration: {formatMinutes(dive.duration)} 31 |
32 |
33 | {#if dive.summary != ''} 34 |
35 | {dive.summary} 36 |
37 | {/if} 38 | 44 |
45 |
46 | 47 | 61 | -------------------------------------------------------------------------------- /front/src/style/spectre/_popovers.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/get-var' as *; 2 | @use 'mixins/shadow' as *; 3 | @use 'variables' as *; 4 | 5 | // Popovers 6 | .popover { 7 | display: inline-block; 8 | position: relative; 9 | 10 | .popover-container { 11 | left: 50%; 12 | opacity: 0; 13 | // padding: $layout-spacing; // old spectre.css 14 | padding: get-var('layout-spacing', $unit: 1); 15 | position: absolute; 16 | top: 0; 17 | transform: translate(-50%, -50%) scale(0); 18 | // transition: transform 0.2s; // old spectre.css 19 | transition: transform get-var('transition-duration'); 20 | // width: $control-width-sm; // old spectre.css 21 | width: get-var('control-width', $suffix: 'sm'); 22 | // z-index: $zindex-3; // old spectre.css 23 | z-index: get-var('z-index-3'); 24 | } 25 | 26 | *:focus + .popover-container, 27 | &:hover .popover-container { 28 | display: block; 29 | opacity: 1; 30 | transform: translate(-50%, -100%) scale(1); 31 | } 32 | 33 | &.popover-right { 34 | .popover-container { 35 | left: 100%; 36 | top: 50%; 37 | } 38 | 39 | *:focus + .popover-container, 40 | &:hover .popover-container { 41 | transform: translate(0, -50%) scale(1); 42 | } 43 | } 44 | 45 | &.popover-bottom { 46 | .popover-container { 47 | left: 50%; 48 | top: 100%; 49 | } 50 | 51 | *:focus + .popover-container, 52 | &:hover .popover-container { 53 | transform: translate(-50%, 0) scale(1); 54 | } 55 | } 56 | 57 | &.popover-left { 58 | .popover-container { 59 | left: 0; 60 | top: 50%; 61 | } 62 | 63 | *:focus + .popover-container, 64 | &:hover .popover-container { 65 | transform: translate(-100%, -50%) scale(1); 66 | } 67 | } 68 | 69 | .card { 70 | @include shadow-variant(0.2rem); 71 | border: 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/escape.rs: -------------------------------------------------------------------------------- 1 | use pulldown_cmark::{Options, Parser}; 2 | 3 | #[inline] 4 | pub fn escape(input: &str) -> String { 5 | let mut result = String::with_capacity(input.len()); 6 | 7 | for c in input.chars() { 8 | match c { 9 | '"' => result.push_str("""), 10 | '\'' => result.push_str("'"), 11 | '&' => result.push_str("&"), 12 | '<' => result.push_str("<"), 13 | '>' => result.push_str(">"), 14 | o => result.push(o), 15 | } 16 | } 17 | result 18 | } 19 | 20 | pub fn md_to_text(input: &str) -> String { 21 | use pulldown_cmark::Event::*; 22 | Parser::new_ext(input, Options::ENABLE_TABLES) 23 | .filter_map(|ev| match ev { 24 | Text(val) => Some(val), 25 | _ => None, 26 | }) 27 | .collect::>() 28 | .join(" ") 29 | } 30 | 31 | pub fn md_to_html(input: &str) -> String { 32 | use pulldown_cmark::Event::*; 33 | 34 | let filter_html = 35 | Parser::new_ext(input, Options::ENABLE_TABLES).filter(|ev| !matches!(ev, Html(_))); 36 | 37 | let mut html_buf = String::new(); 38 | 39 | pulldown_cmark::html::push_html(&mut html_buf, filter_html); 40 | 41 | html_buf 42 | } 43 | 44 | pub fn truncate(input: &str, len: usize) -> String { 45 | if input.len() <= len { 46 | return input.to_string(); 47 | } 48 | 49 | let mut end_idx = len + 1; 50 | 51 | while !input.is_char_boundary(end_idx) { 52 | end_idx -= 1; 53 | } 54 | 55 | let slice = &input[0..end_idx]; 56 | 57 | let mut end_idx = len; 58 | 59 | if let Some(val) = slice.rfind(char::is_whitespace) { 60 | end_idx = val; 61 | } 62 | 63 | format!("{}...", &input[0..end_idx]) 64 | } 65 | -------------------------------------------------------------------------------- /front/src/style/spectre/_columns-order.scss: -------------------------------------------------------------------------------- 1 | @use 'variables' as *; 2 | 3 | // Columns order 4 | @for $i from 1 through $columns-count { 5 | .order-#{$i} { 6 | -ms-flex-order: $i; 7 | order: $i; 8 | } 9 | } 10 | 11 | @media (max-width: ($size-xs)) { 12 | @for $i from 1 through $columns-count { 13 | .order-#{$i} { 14 | -ms-flex-order: $i; 15 | order: $i; 16 | } 17 | .order-xs-#{$i} { 18 | -ms-flex-order: #{$i}; 19 | order: #{$i}; 20 | } 21 | } 22 | } 23 | 24 | @media (max-width: ($size-sm)) { 25 | @for $i from 1 through $columns-count { 26 | .order-#{$i} { 27 | -ms-flex-order: $i; 28 | order: $i; 29 | } 30 | .order-sm-#{$i} { 31 | -ms-flex-order: #{$i}; 32 | order: #{$i}; 33 | } 34 | } 35 | } 36 | 37 | @media (max-width: ($size-md)) { 38 | @for $i from 1 through $columns-count { 39 | .order-#{$i} { 40 | -ms-flex-order: $i; 41 | order: $i; 42 | } 43 | .order-md-#{$i} { 44 | -ms-flex-order: #{$i}; 45 | order: #{$i}; 46 | } 47 | } 48 | } 49 | 50 | @media (max-width: ($size-lg)) { 51 | @for $i from 1 through $columns-count { 52 | .order-#{$i} { 53 | -ms-flex-order: $i; 54 | order: $i; 55 | } 56 | .order-lg-#{$i} { 57 | -ms-flex-order: #{$i}; 58 | order: #{$i}; 59 | } 60 | } 61 | } 62 | 63 | @media (max-width: ($size-xl)) { 64 | @for $i from 1 through $columns-count { 65 | .order-#{$i} { 66 | -ms-flex-order: $i; 67 | order: $i; 68 | } 69 | .order-xl-#{$i} { 70 | -ms-flex-order: #{$i}; 71 | order: #{$i}; 72 | } 73 | } 74 | } 75 | 76 | @media (max-width: ($size-2x)) { 77 | @for $i from 1 through $columns-count { 78 | .order-#{$i} { 79 | -ms-flex-order: $i; 80 | order: $i; 81 | } 82 | .order-2x-#{$i} { 83 | -ms-flex-order: #{$i}; 84 | order: #{$i}; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "divedb", 3 | "license": "AGPL-3.0", 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "format": "prettier --write .", 13 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 14 | "generate": "graphql-codegen", 15 | "lint": "prettier --check ." 16 | }, 17 | "devDependencies": { 18 | "@graphql-codegen/cli": "^5.0.7", 19 | "@graphql-codegen/typescript": "^4.1.6", 20 | "@graphql-codegen/typescript-graphql-request": "^6.3.0", 21 | "@graphql-codegen/typescript-operations": "^4.6.1", 22 | "@sveltejs/adapter-auto": "^6.0.0", 23 | "@sveltejs/adapter-node": "^5.2.14", 24 | "@sveltejs/kit": "^2.22.0", 25 | "@sveltejs/vite-plugin-svelte": "^6.0.0", 26 | "@types/leaflet": "^1.9.20", 27 | "@types/lodash-es": "^4.17.12", 28 | "prettier": "^3.4.2", 29 | "prettier-plugin-svelte": "^3.3.3", 30 | "sass": "^1.90.0", 31 | "svelte": "^5.0.0", 32 | "svelte-check": "^4.0.0", 33 | "svelte-preprocess": "^6.0.3", 34 | "typescript": "^5.5.0", 35 | "vite": "^7.0.4" 36 | }, 37 | "dependencies": { 38 | "@geoman-io/leaflet-geoman-free": "^2.18.3", 39 | "bytemd": "^1.22.0", 40 | "express": "^5.1.0", 41 | "graphql": "^16.11.0", 42 | "graphql-request": "^7.2.0", 43 | "graphql-tag": "^2.12.6", 44 | "http-proxy-middleware": "^3.0.5", 45 | "leaflet": "^1.9.4", 46 | "lodash-es": "^4.17.21", 47 | "rehype-sanitize": "^6.0.0", 48 | "rehype-stringify": "^10.0.1", 49 | "remark-parse": "^11.0.0", 50 | "remark-rehype": "^11.1.2", 51 | "swiper": "^8.4.7", 52 | "unified": "^11.0.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /front/src/style/spectre/utilities/_loading.scss: -------------------------------------------------------------------------------- 1 | @use '../functions/color' as *; 2 | @use '../functions/get-var' as *; 3 | @use '../functions/var-negative' as *; 4 | @use '../variables' as *; 5 | 6 | // Loading 7 | .loading { 8 | color: transparent !important; 9 | // min-height: $unit-4; // old spectre.css 10 | min-height: get-var('unit-4'); 11 | pointer-events: none; 12 | position: relative; 13 | &::after { 14 | animation: loading 500ms infinite linear; 15 | background: transparent; 16 | // border: $border-width-lg solid $primary-color; // old spectre.css 17 | border: get-var('border-width', $suffix: 'lg') solid color('primary-color'); 18 | border-radius: 50%; 19 | border-right-color: transparent; 20 | border-top-color: transparent; 21 | content: ''; 22 | display: block; 23 | // height: $unit-4; // old spectre.css 24 | height: get-var('unit-4'); 25 | left: 50%; 26 | // margin-left: -$unit-2; // old spectre.css 27 | margin-left: var-negative(get-var('unit-2')); 28 | // margin-top: -$unit-2; // old spectre.css 29 | margin-top: var-negative(get-var('unit-2')); 30 | opacity: 1; 31 | padding: 0; 32 | position: absolute; 33 | top: 50%; 34 | // width: $unit-4; // old spectre.css 35 | width: get-var('unit-4'); 36 | // z-index: $zindex-0; // old spectre.css 37 | z-index: get-var('z-index-0'); 38 | } 39 | 40 | &.loading-lg { 41 | // min-height: $unit-10; // old spectre.css 42 | min-height: get-var('unit-10'); 43 | &::after { 44 | // height: $unit-8; // old spectre.css 45 | height: get-var('unit-8'); 46 | // margin-left: -$unit-4; // old spectre.css 47 | margin-left: var-negative(get-var('unit-4')); 48 | // margin-top: -$unit-4; // old spectre.css 49 | margin-top: var-negative(get-var('unit-4')); 50 | // width: $unit-8; // old spectre.css 51 | width: get-var('unit-8'); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /front/src/style/spectre/_cards.scss: -------------------------------------------------------------------------------- 1 | @use 'functions/color' as *; 2 | @use 'functions/get-var' as *; 3 | @use 'mixins/shadow' as *; 4 | @use 'variables' as *; 5 | 6 | // Cards 7 | .card { 8 | // background: $bg-color-light; // old spectre.css 9 | background: color('bg-color-light'); 10 | // border: $border-width solid $border-color; // old spectre.css 11 | border: get-var('border-width') solid color('border-color'); 12 | // border-radius: $border-radius; // old spectre.css 13 | border-radius: get-var('border-radius'); 14 | display: flex; 15 | flex-direction: column; 16 | 17 | .card-header, 18 | .card-body, 19 | .card-footer { 20 | // padding: $layout-spacing-lg; // old spectre.css 21 | padding: get-var('layout-spacing', $suffix: 'lg', $unit: 1); 22 | padding-bottom: 0; 23 | 24 | &:last-child { 25 | // padding-bottom: $layout-spacing-lg; // old spectre.css 26 | padding-bottom: get-var('layout-spacing', $suffix: 'lg', $unit: 1); 27 | } 28 | } 29 | 30 | .card-body { 31 | flex: 1 1 auto; 32 | } 33 | 34 | .card-image { 35 | // padding-top: $layout-spacing-lg; // old spectre.css 36 | padding-top: get-var('layout-spacing', $suffix: 'lg', $unit: 1); 37 | 38 | &:first-child { 39 | padding-top: 0; 40 | 41 | img { 42 | // border-top-left-radius: $border-radius; // old spectre.css 43 | border-top-left-radius: get-var('border-radius'); 44 | // border-top-right-radius: $border-radius; // old spectre.css 45 | border-top-right-radius: get-var('border-radius'); 46 | } 47 | } 48 | 49 | &:last-child { 50 | img { 51 | // border-bottom-left-radius: $border-radius; // old spectre.css 52 | border-bottom-left-radius: get-var('border-radius'); 53 | // border-bottom-right-radius: $border-radius; // old spectre.css 54 | border-bottom-right-radius: get-var('border-radius'); 55 | } 56 | } 57 | } 58 | 59 | &.card-shadow { 60 | @include shadow-variant(0.1rem); 61 | border: 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/db/migrations/create_apub_keys.rs: -------------------------------------------------------------------------------- 1 | use activitypub_federation::http_signatures::generate_actor_keypair; 2 | use anyhow::Error; 3 | use async_trait::async_trait; 4 | use deadpool_postgres::Pool; 5 | use divedb_core::FromRow; 6 | use uuid::Uuid; 7 | 8 | use super::Migration; 9 | 10 | pub struct CreateApubKeys; 11 | 12 | #[async_trait] 13 | impl Migration for CreateApubKeys { 14 | fn name(&self) -> &str { 15 | "create_apub_keys" 16 | } 17 | 18 | async fn migrate(&self, pool: &Pool) -> Result<(), Error> { 19 | let mut client = pool.get().await?; 20 | 21 | client 22 | .batch_execute( 23 | " 24 | alter table users add column public_key text; 25 | alter table users add column private_key text; 26 | ", 27 | ) 28 | .await?; 29 | 30 | let result = client.query("select id from users", &[]).await?; 31 | 32 | let users = User::from_rows(result)?; 33 | 34 | let transaction = client.transaction().await?; 35 | let statement = transaction 36 | .prepare("update users set public_key = $1, private_key = $2 where id = $3") 37 | .await?; 38 | 39 | for user in users { 40 | let key_pair = generate_actor_keypair()?; 41 | 42 | transaction 43 | .execute( 44 | &statement, 45 | &[&key_pair.public_key, &key_pair.private_key, &user.id], 46 | ) 47 | .await?; 48 | } 49 | 50 | transaction.commit().await?; 51 | 52 | client 53 | .batch_execute( 54 | " 55 | alter table users alter column public_key set not null; 56 | ", 57 | ) 58 | .await?; 59 | 60 | Ok(()) 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone, FromRow)] 65 | pub struct User { 66 | pub id: Uuid, 67 | } 68 | -------------------------------------------------------------------------------- /front/src/style/spectre/_progress.scss: -------------------------------------------------------------------------------- 1 | @use 'functions' as *; 2 | @use 'variables' as *; 3 | 4 | // Progress 5 | // Credit: https://css-tricks.com/html5-progress-element/ 6 | .progress { 7 | appearance: none; 8 | // background: $bg-color-dark; // old spectre.css 9 | background: color('bg-color-dark'); 10 | border: 0; 11 | // border-radius: $border-radius; // old spectre.css 12 | border-radius: get-var('border-radius'); 13 | // color: $primary-color; // old spectre.css 14 | color: color('primary-color'); 15 | // height: $unit-1; // old spectre.css 16 | height: get-var('unit-1'); 17 | position: relative; 18 | width: 100%; 19 | 20 | &::-webkit-progress-bar { 21 | background: transparent; 22 | // border-radius: $border-radius; // old spectre.css 23 | border-radius: get-var('border-radius'); 24 | } 25 | 26 | &::-webkit-progress-value { 27 | // background: $primary-color; // old spectre.css 28 | background: color('primary-color'); 29 | // border-radius: $border-radius; // old spectre.css 30 | border-radius: get-var('border-radius'); 31 | } 32 | 33 | &::-moz-progress-bar { 34 | // background: $primary-color; // old spectre.css 35 | background: color('primary-color'); 36 | // border-radius: $border-radius; // old spectre.css 37 | border-radius: get-var('border-radius'); 38 | } 39 | 40 | &:indeterminate { 41 | animation: progress-indeterminate 1.5s linear infinite; 42 | // TODO: Gradient. 43 | // background: color('bg-color-dark') linear-gradient(to right, $primary-color 30%, $bg-color-dark 30%) top left / 150% 150% no-repeat; 44 | background: color('bg-color-dark') 45 | linear-gradient(to right, color('primary-color') 30%, color('bg-color-dark') 30%) top left / 46 | 150% 150% no-repeat; 47 | 48 | &::-moz-progress-bar { 49 | background: transparent; 50 | } 51 | } 52 | } 53 | 54 | @keyframes progress-indeterminate { 55 | 0% { 56 | background-position: 200% 0; 57 | } 58 | 100% { 59 | background-position: -200% 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /front/src/routes/verify-email/+page.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | DiveDB - Email Verification 43 | 44 | 45 |
46 |
47 |
48 |

Email Verification

49 |
50 |
51 |
52 |
53 |

Set a new password below to finish resetting your password

54 |
55 |
56 |
57 | {#if errors} 58 |
{errors}
59 | {/if} 60 | {#if loading} 61 |
62 |
63 |
64 | {/if} 65 |
66 |
67 | -------------------------------------------------------------------------------- /front/src/routes/feedback/+page.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

Add Feedback

37 |
38 | 39 | {#if submitted} 40 |
Submitted! Thanks for your feedback
41 | {:else} 42 |
43 | 49 |
50 |
51 | 54 |
55 | {/if} 56 | {#if errors} 57 |
58 |
{errors}
59 |
60 | {/if} 61 | {#if loading} 62 |
63 |
64 |
65 | {/if} 66 |
67 |
68 | 69 | 74 | --------------------------------------------------------------------------------