Compare commits

...

63 Commits

Author SHA1 Message Date
6188e0012a style: fix leaflet control overlap
All checks were successful
/ Build (push) Successful in 35s
2024-09-03 08:45:22 +03:00
cea91dbf97 style: fix input element sizing 2024-09-03 08:41:22 +03:00
aaaa2cf597 chore: add url to readme
All checks were successful
/ Build (push) Successful in 36s
2024-08-28 19:16:12 +03:00
f65596963f chore: update urls
All checks were successful
/ Build (push) Successful in 35s
2024-08-28 19:09:50 +03:00
62f60c7491 fix: type diameter to radius
All checks were successful
/ Build (push) Successful in 1m0s
2024-08-06 17:11:47 +03:00
cbe4ebc8d1 feat: check location according to radius 2024-08-06 17:04:34 +03:00
aeb1b32441 fix: change diameter to radius 2024-08-06 16:53:04 +03:00
1431aab04a feat: add hidden input for geolocation diameter 2024-08-06 16:32:46 +03:00
f1e16e8c74 feat: add diameter control 2024-08-06 16:30:06 +03:00
0330439362 chore: change site on dev mode 2024-08-06 13:46:28 +03:00
8f61c0f99f refactor: use native form post instead if ajax
All checks were successful
/ Build (push) Successful in 34s
2024-08-05 12:17:26 +03:00
37946d65b2 chore: update astro
All checks were successful
/ Build (push) Successful in 36s
2024-08-05 09:54:16 +03:00
9c6a574708 fix: meta info
All checks were successful
/ Build (push) Successful in 34s
2024-07-29 23:01:27 +03:00
1f282bf07a fix: get from search param
All checks were successful
/ Build (push) Successful in 41s
2024-07-29 22:42:36 +03:00
08a82bfcf3 chore: change url
All checks were successful
/ Build (push) Successful in 44s
2024-07-29 22:19:00 +03:00
165d969c84 fix: hardcode icon urls
All checks were successful
/ Build (push) Successful in 42s
2024-07-29 22:11:44 +03:00
4250cc42db fix: type in deploy script
All checks were successful
/ Build (push) Successful in 40s
2024-07-29 21:54:00 +03:00
2456dcaccc feat: hardcode env
All checks were successful
/ Build (push) Successful in 38s
2024-07-29 21:52:05 +03:00
9674be37cf fix: get backend url from env
All checks were successful
/ Build (push) Successful in 34s
2024-07-29 21:39:07 +03:00
d9cc6c2d30 ci: remove email step from deploy script
All checks were successful
/ Build (push) Successful in 1m2s
2024-07-29 20:05:43 +03:00
6ea4390223 chore: remove stale import statement
Some checks failed
/ Build (push) Failing after 1m5s
2024-07-29 19:20:05 +03:00
21df3f0826 ci: enable corepack in setup script
Some checks failed
/ Build (push) Failing after 47s
2024-07-29 19:18:24 +03:00
1e19829f19 ci: use yarn
Some checks failed
/ Build (push) Failing after 7s
2024-07-29 19:13:55 +03:00
616b64a742 ci: add deploy script
Some checks failed
/ Build (push) Failing after 29s
2024-07-29 19:06:56 +03:00
64c4203b21 chore: get urls from environment 2024-07-29 18:42:23 +03:00
360c51cbea feat: remove remote stylesheets 2024-07-29 18:34:32 +03:00
892b719222 feat: replace dynamic page with a static page 2024-07-29 18:23:47 +03:00
466c4eb51d style: mobile styling 2024-07-28 21:54:37 +03:00
0101387b46 style: bigger font sizes 2024-07-28 20:32:56 +03:00
dfa914af5a style: bigger font styles 2024-07-28 20:14:18 +03:00
318d1caa9e chore: move index.astro script to a seperate file
feat: add custom validation for input elements
2024-07-28 17:00:10 +03:00
7cb50bd982 reefactor: initSelectionMap 2024-07-27 15:13:52 +03:00
9c74cc7263 chore: remove unnecessary packages
style: run prettier
2024-07-27 14:25:28 +03:00
86cc813977 feat: add geolocatoin permission denied case for some elements 2024-07-26 21:48:34 +03:00
820cdc903b refactor: add utility classes for dom access 2024-07-26 21:23:56 +03:00
193b8604b6 feat: add DOM utility functions 2024-07-26 00:03:56 +03:00
9e798f1b15 refactor: remove stale code 2024-07-25 19:53:59 +03:00
5097f70d90 feat: increment counter on unlock button click 2024-07-24 12:00:12 +03:00
05a5b436d1 feat: reveal image when unlock button is clicked 2024-07-23 22:45:07 +03:00
e6b140068b feat: add css transition for unlock buton 2024-07-23 22:32:26 +03:00
4f5a437c7c refactor: move lockedcontent webcomponent to standard html 2024-07-23 18:27:06 +03:00
d5bc7b98dd refactor: remove unnecessary control from leaflet-map 2024-07-22 14:29:29 +03:00
09e69f61b9 feat: update geolocation from body 2024-07-22 12:45:17 +03:00
622c436a9f feat: remove geolocation logic from components 2024-07-21 20:11:44 +03:00
100d329d47 feat: merge geolocation and ask permission controls 2024-07-21 19:22:41 +03:00
ecad060770 feat: add geolocation permission control 2024-07-21 13:24:16 +03:00
fb31a822a0 feat: add new control to leaflet web component 2024-07-21 11:45:55 +03:00
d0df5fbbc6 feat: move leaflet map to a web component 2024-07-19 16:38:58 +03:00
430c50b3ae feat: synchronize map permissoins with lockedcontent component permissons 2024-07-19 13:18:44 +03:00
7e77b4326f feat: add permission denied state to locked content 2024-07-18 20:13:26 +03:00
c6f0d851c2 feat: add client side validation for file input 2024-07-18 20:00:48 +03:00
e7e449a079 feat: refactor locked content component 2024-07-18 14:02:45 +03:00
0539bedc84 feat: add standalone templates 2024-07-17 18:23:55 +03:00
e406ed40a6 feat: add increment counter feature 2024-07-17 17:12:48 +03:00
9bc380c81d feat: manage permissons 2024-07-17 13:36:56 +03:00
5ea7471678 feat: add templates and geolocation callbacks to lit component 2024-07-17 13:02:51 +03:00
873e127251 feat: create a lit component 2024-07-11 13:49:35 +03:00
6aae6b37bd feat: add geolocation features to locked content 2024-07-11 10:47:04 +03:00
ad19793879 feat: show content on click 2024-07-10 14:55:57 +03:00
59090b979c feat: locked-content repond to attribute changes 2024-07-10 13:59:13 +03:00
5821818618 feat: add templates for buttons 2024-07-06 15:56:58 +03:00
68750ad964 refactor: replace shadui components with standard html elements
feat: add new unlock button web component
2024-07-06 13:39:36 +03:00
296083577a chore: change endpoint 2024-07-05 18:01:22 +03:00
38 changed files with 5194 additions and 1127 deletions

View File

@ -0,0 +1,79 @@
run-name: ${{ gitea.actor }}, deploy with ssh
on:
push:
branches:
- "release"
env:
BUILD_PATH: "."
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect package manager
id: detect-package-manager
run: |
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
echo "manager=yarn" >> $GITHUB_OUTPUT
echo "command=install" >> $GITHUB_OUTPUT
echo "runner=yarn" >> $GITHUB_OUTPUT
echo "lockfile=yarn.lock" >> $GITHUB_OUTPUT
exit 0
elif [ -f "${{ github.workspace }}/package.json" ]; then
echo "manager=npm" >> $GITHUB_OUTPUT
echo "command=ci" >> $GITHUB_OUTPUT
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
echo "lockfile=package-lock.json" >> $GITHUB_OUTPUT
exit 0
else
echo "Unable to determine package manager"
exit 1
fi
- name: Install package manager (from package.json)
run: |
corepack enable
corepack install
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: ${{ steps.detect-package-manager.outputs.manager }}
cache-dependency-path:
${{ env.BUILD_PATH }}/${{
steps.detect-package-manager.outputs.lockfile }}
- name: Install dependencies
run:
${{ steps.detect-package-manager.outputs.manager }} ${{
steps.detect-package-manager.outputs.command }}
working-directory: ${{ env.BUILD_PATH }}
- name: Make envfile
uses: SpicyPizza/create-envfile@v2.0
with:
envkey_PUBLIC_BACKEND_URL: ${{ vars.PUBLIC_BACKEND_URL }}
- name: Build with Astro
run: |
${{ steps.detect-package-manager.outputs.runner }} astro build
working-directory: ${{ env.BUILD_PATH }}
- name: Deploy to my server
id: deploy
uses: up9cloud/action-rsync@master
env:
USER: ${{ secrets.USERNAME }}
HOST: ${{ secrets.HOST }}
KEY: ${{ secrets.KEY }}
SOURCE: ./dist/
TARGET: ${{ secrets.DESTINATION_FOLDER }}
ARGS: -avz --exclude=/.git/
SSH_ARGS:
"-p 22 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
VERBOSE: true
PRE_SCRIPT: |
echo start at:
date -u
POST_SCRIPT: "echo done at: && date -u"

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ pnpm-debug.log*
# macOS-specific files # macOS-specific files
.DS_Store .DS_Store
TODO

View File

@ -1,18 +1,18 @@
{ {
"endOfLine": "lf", "endOfLine": "lf",
"printWidth": 120, "printWidth": 80,
"arrowParens": "avoid", "arrowParens": "always",
"bracketSpacing": true, "bracketSpacing": true,
"htmlWhitespaceSensitivity": "css", "htmlWhitespaceSensitivity": "css",
"insertPragma": false, "insertPragma": false,
"bracketSameLine": true, "bracketSameLine": true,
"jsxSingleQuote": true, "jsxSingleQuote": true,
"proseWrap": "preserve", "proseWrap": "always",
"quoteProps": "as-needed", "quoteProps": "as-needed",
"requirePragma": false, "requirePragma": false,
"semi": false, "semi": false,
"singleQuote": false, "singleQuote": false,
"tabWidth": 2, "tabWidth": 2,
"trailingComma": "none", "trailingComma": "es5",
"useTabs": false "useTabs": false
} }

View File

@ -1,54 +1,3 @@
# Astro Starter Kit: Basics # KONULU KONUM
```sh https://konulukonum.com
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@ -2,13 +2,13 @@ import { defineConfig } from 'astro/config';
import react from "@astrojs/react"; import react from "@astrojs/react";
import tailwind from "@astrojs/tailwind"; import tailwind from "@astrojs/tailwind";
import vercel from "@astrojs/vercel/serverless"; const devMode = import.meta.env.DEV
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [react(), tailwind({ integrations: [react(), tailwind({
applyBaseStyles: false applyBaseStyles: false
})], })],
output: "server", output: "static",
adapter: vercel() site: devMode ? "http://localhost:4321" : "https://konulukonum.com"
}); });

BIN
bun.lockb

Binary file not shown.

View File

@ -11,36 +11,33 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.4.1", "@astrojs/check": "^0.4.1",
"@astrojs/react": "^3.0.9", "@astrojs/react": "^3.6.0",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@astrojs/vercel": "^7.3.1", "@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.1.0",
"@supabase/supabase-js": "^2.39.7", "astro": "^4.11.6",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@vercel/blob": "^0.19.0",
"@vercel/postgres-kysely": "^0.7.1",
"@vercel/speed-insights": "^1.0.9",
"astro": "^4.1.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.11",
"kysely": "^0.26.0", "leaflet": "^1.9.4",
"lucide-react": "^0.309.0", "lucide-react": "^0.309.0",
"nanoid": "^5.0.4", "react": "^18.3.1",
"react": "^18.2.0", "react-dom": "^18.3.1",
"react-dom": "^18.2.0", "tailwind-merge": "^2.4.0",
"tailwind-merge": "^2.2.0", "tailwindcss": "^3.4.6",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.3" "toastify-js": "^1.12.0",
"typescript": "^5.5.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.0.5", "@types/leaflet": "^1.9.12",
"@types/google.maps": "^3.54.10" "@types/react": "^18.3.3",
} "@types/react-dom": "^18.3.0",
"@types/google.maps": "^3.55.11",
"@types/toastify-js": "^1.12.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

View File

@ -1,61 +0,0 @@
---
interface Props {
title: string;
body: string;
href: string;
}
const { href, title, body } = Astro.props;
---
<li class="link-card">
<a href={href}>
<h2>
{title}
<span>&rarr;</span>
</h2>
<p>
{body}
</p>
</a>
</li>
<style>
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card > a {
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
</style>

View File

@ -0,0 +1,10 @@
import L from "leaflet"
export const openstreetmapTiles = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}
)

View File

@ -0,0 +1,18 @@
import L from "leaflet"
function updateMarkerLocation(
marker: L.Marker,
icon: L.Icon,
position: L.LatLng,
map: L.Map
) {
if (marker) {
marker.setLatLng(position)
} else {
marker = L.marker(position, { icon })
marker.addTo(map)
}
return marker
}
export { updateMarkerLocation }

View File

@ -0,0 +1,13 @@
import L from "leaflet"
var targetLocationIcon = L.icon({
iconUrl: "/goal.svg",
iconSize: [32, 32],
})
var currentLocationIcon = L.icon({
iconUrl: "/blue-dot.png",
iconSize: [32, 32],
})
export { targetLocationIcon, currentLocationIcon }

View File

@ -1,147 +0,0 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { LockClosedIcon, LockOpen1Icon } from "@radix-ui/react-icons"
import { useEffect, useState } from "react"
import "../styles/locked-content.css"
import type { Generated } from "kysely"
import { onLocationError } from "@/lib/error"
const incrementCounter = async (id: string | Generated<string>) =>
await fetch(`${import.meta.env.PUBLIC_HOME_URL}/api/content/increment?id=${id}`)
const LocationButton = ({
contentId = "",
imageUrl = "#",
location = ""
}: {
contentId?: string | Generated<string>
imageUrl?: string
location?: string
}) => {
const [atTarget, setAtTarget] = useState(false)
const [contentVisible, setContentVisible] = useState(false)
const [hasPermission, setHasPermission] = useState(false)
const [watchId, setWatchId] = useState<number>()
const [distanceRemain, setDistanceRemain] = useState<string>("")
const targetCoordinates = JSON.parse(location).coordinates
const targetPos = {
lat: targetCoordinates[0],
lng: targetCoordinates[1]
}
const startWatchingLocation = () => {
setHasPermission(true)
if (!watchId) {
const id = navigator.geolocation.watchPosition(
(position: GeolocationPosition) => {
const pos = {
lat: position.coords.latitude,
lng: position.coords.longitude
}
// @ts-expect-error 3rd party script
const targetLatLng = L.latLng(targetPos)
// @ts-expect-error 3rd party script
const currentLatLng = L.latLng(pos)
const betweenMeters = currentLatLng.distanceTo(targetLatLng)
if (betweenMeters > 1000) {
setDistanceRemain(`${(betweenMeters / 1000).toFixed()} KM`)
} else if (betweenMeters > 50) {
setDistanceRemain(`${betweenMeters.toFixed(0)} M`)
} else {
setAtTarget(true)
}
},
err => onLocationError(err),
{ enableHighAccuracy: true, timeout: 27000, maximumAge: 10000 }
)
setWatchId(id)
}
}
const handleUnlock = async () => {
setContentVisible(true)
await incrementCounter(contentId)
}
useEffect(() => {
navigator.permissions.query({ name: "geolocation" }).then(permissionStatus => {
if (permissionStatus.state === "granted") {
setHasPermission(true)
startWatchingLocation()
}
})
}, [])
if (contentVisible) {
return (
<div className='w-full h-[475px] p-4 flex justify-center'>
<img src={imageUrl} />
</div>
)
} else {
return (
<div className='w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded'>
{atTarget ? (
<div className='flex flex-col justify-center items-center image-wrapper'>
<img src={imageUrl} className='blur-2xl h-[450px]' />
<div className='flex flex-col justify-center gap-4 overlay'>
<Button
size='lg'
className='text-lg p-6 animate-pulse bg-indigo-600 hover:bg-indigo-700 hover:animate-none border-2 border-indigo-800'
onClick={handleUnlock}>
<LockOpen1Icon className='mr-2 h-4 w-4' />
İçeriğin Kilidi ıldı
</Button>
<Card className='p-2'>
<CardContent className='pb-0 text-center'>İçeriği görmek için butona bas!</CardContent>
</Card>
</div>
</div>
) : (
<div className='flex flex-col justify-center items-center image-wrapper'>
<img src={imageUrl} className='blur-2xl h-[450px]' />
<div className='flex flex-col justify-center gap-4 overlay'>
<Button size='lg' className='text-md'>
<LockClosedIcon className='mr-2 h-4 w-4' /> İçerik Kilitli
</Button>
<Card className='p-2'>
{hasPermission ? (
<CardContent className='pb-0 text-center'>
<p>İçeriği görmek için konuma gitmelisin!</p>
<p>{distanceRemain && `Kalan mesafe: ${distanceRemain}`}</p>
</CardContent>
) : (
<div className='flex flex-col gap-2'>
<CardContent className='pb-0 text-center'>
Ne kadar yaklaştığını görmek için aşağıdaki butona bas.
</CardContent>
<Button
size='sm'
className='bg-green-700 hover:bg-green-600 text-md'
onClick={() => startWatchingLocation()}>
Konum İzni Ver
</Button>
</div>
)}
</Card>
</div>
</div>
)}
</div>
)
}
}
export default LocationButton

View File

@ -0,0 +1,133 @@
import L, { type LatLngTuple } from "leaflet"
import {
addAttribute,
addClasses,
removeClasses,
removeElement,
revealContent,
toggleClass,
updateText,
} from "../../lib/domUtils"
import { mapLocationSuccessCallback } from "@/scripts/initMap"
import { toast } from "@/lib/utils"
// Update the elements according to distance remaining
function locationSuccessCallback(
position: GeolocationPosition,
targetPosition: LatLngTuple,
radius: number
) {
// Enable current location control
removeClasses("current-location-control", "disabled-button")
const newPosition = position.coords
// Calculate the distance remaining
const distance = calculateDistance(
[newPosition.latitude, newPosition.longitude],
targetPosition
)
// If user has arrived to destination
if (distance < radius) {
// Change the description texts
updateText("button-text", "İçeriği Göster")
updateText("locked-content-description", "İçeriği görmek için butona bas!")
// Swap the icon
toggleClass("lock-icon", "hidden")
toggleClass("unlock-icon", "hidden")
// Tansform the unlock button
removeClasses("unlock-content-button", "bg-primary", "hover:bg-primary/90")
addClasses(
"unlock-content-button",
"bg-indigo-600",
"hover:bg-indigo-700",
"hover:animate-none",
"border-2",
"border-indigo-800"
)
// Wait for transition to finish then add animation
setTimeout(() => {
removeClasses("unlock-content-button", "duration-1000")
addClasses("unlock-content-button", "animate-pulse")
}, 800)
// Reveal image when the unlock button is clicked
const unlockButton = document.getElementById("unlock-content-button")
unlockButton?.addEventListener("click", revealContent)
} else {
const distanceText = generateDistanceText(distance)
updateText("locked-content-description", `Kalan mesafe: ${distanceText}`)
}
removeElement("location-permission-button", true)
// Update leaflet controls
mapLocationSuccessCallback(position)
}
// This callback will be fired on geolocation error
function errorCallback(err: GeolocationPositionError) {
let errorMessage
// Show toast accoring to the error state
switch (err.code) {
case GeolocationPositionError.PERMISSION_DENIED:
errorMessage =
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
updateText(
"locked-content-description",
"Konum izleme izni alınamadı. \nİçeriği görüntüleyebilmek için konum bilginiz gerekiyor."
)
addAttribute("current-location-control", "disabled", "true")
addClasses("current-location-control", "disabled-button")
removeElement("location-permission-button")
break
case GeolocationPositionError.POSITION_UNAVAILABLE:
errorMessage =
"Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz."
break
case GeolocationPositionError.TIMEOUT:
return
default:
errorMessage =
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
break
}
toast(errorMessage)
}
function calculateDistance(
currentPosition: L.LatLngTuple,
targetPosition: L.LatLngTuple
) {
// Get target position in latitudes and longitudes
const targetLatLng = L.latLng(targetPosition)
// Get current position in latitudes and longitudes
const currentLatLng = L.latLng(currentPosition)
// Calculate the distance between target and current position in meters
const betweenMeters = currentLatLng.distanceTo(targetLatLng)
return betweenMeters
}
// Generates a human readable destination text according to
// distance remaining
function generateDistanceText(distance: number) {
if (distance > 1000) {
return `${(distance / 1000).toFixed()} KM`
} else if (distance > 100) {
return `${distance.toFixed(0)} M`
}
}
export {
errorCallback,
locationSuccessCallback,
calculateDistance,
generateDistanceText,
}

View File

@ -0,0 +1,14 @@
// This counter is shown at the bottom of the page and incremented
// each time "show content" button is clicked
function incrementUnlockCounter(id: string | undefined) {
if (id) {
fetch(
`${import.meta.env.PUBLIC_BACKEND_URL}/api/location/increment/${id}`,
{
method: "PATCH",
}
)
}
}
export { incrementUnlockCounter }

View File

@ -1,3 +1,4 @@
import { toast } from "@/lib/utils"
import { Button } from "./ui/button" import { Button } from "./ui/button"
const ShareButton = () => { const ShareButton = () => {
@ -5,7 +6,7 @@ const ShareButton = () => {
const shareData = { const shareData = {
title: "Konulu Konum", title: "Konulu Konum",
text: "Sevdiklerinizi şaşırtın!", text: "Sevdiklerinizi şaşırtın!",
url: document.URL url: document.URL,
} }
await navigator.share(shareData) await navigator.share(shareData)
@ -14,20 +15,7 @@ const ShareButton = () => {
const copyLink = async () => { const copyLink = async () => {
try { try {
await navigator.clipboard.writeText(document.URL) await navigator.clipboard.writeText(document.URL)
// @ts-ignore toast("Konulu konum kopyalandı.")
Toastify({
text: "Konulu konum kopyalandı.",
duration: 3000,
gravity: "top", // `top` or `bottom`
position: "center", // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: "black",
borderRadius: "6px",
margin: "16px"
},
onClick: function () {} // Callback after click
}).showToast()
} catch (err: any) { } catch (err: any) {
console.error(err.message) console.error(err.message)
} }
@ -38,11 +26,11 @@ const ShareButton = () => {
{ {
//@ts-expect-error navigator is not always defined //@ts-expect-error navigator is not always defined
window.navigator.share ? ( window.navigator.share ? (
<Button className='w-full text-lg' size='lg' onClick={shareLink}> <Button className='w-full text-xl' size='lg' onClick={shareLink}>
Paylaş Paylaş
</Button> </Button>
) : ( ) : (
<Button className='w-full text-lg' size='lg' onClick={copyLink}> <Button className='w-full text-xl' size='lg' onClick={copyLink}>
Paylaş Paylaş
</Button> </Button>
) )

9
src/env.d.ts vendored
View File

@ -1,3 +1,12 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
/// <reference types="@types/google.maps" /> /// <reference types="@types/google.maps" />
/// <reference types="@types/leaflet" /> /// <reference types="@types/leaflet" />
export declare global {
namespace astroHTML.JSX {
interface HTMLAttributes {
_?: any
}
}
}

View File

@ -1,47 +1,28 @@
--- ---
import SpeedInsights from '@vercel/speed-insights/astro'; import "leaflet/dist/leaflet.css"
import "toastify-js/src/toastify.css"
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang='en'>
<head> <head>
<meta charset="UTF-8" /> <meta charset='UTF-8' />
<meta name="description" content="Astro description" /> <meta name='description' content='Astro description' />
<meta name="viewport" content="width=device-width" /> <meta name='viewport' content='width=device-width' />
<link rel="icon" type="image/jpg" href="/konulu-konum-logo.jpg" /> <link rel='icon' type='image/jpg' href='/konulu-konum-logo.jpg' />
<link <meta property='og:title' content='Konulu Konum' />
rel="stylesheet" <meta property='og:description' content='Sevdiklerinizi şaşırtın!' />
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" <meta property='og:url' content='https://konulukonum.com' />
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"
/>
<script
is:inline
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<meta property="og:title" content="Konulu Konum" />
<meta property="og:description" content="Sevdiklerinizi şaşırtın!" />
<meta property="og:url" content="https://konulu-konum.vercel.app" />
<meta <meta
property="og:image" property='og:image'
content="https://konulu-konum.vercel.app/konulu-konum-logo.jpg" content='https://konulukonum.com/konulu-konum-logo.jpg'
/> />
<meta name="twitter:card" content="summary" /> <meta name='twitter:card' content='summary' />
<meta name="generator" content={Astro.generator} /> <meta name='generator' content={Astro.generator} />
<title>Konulu Konum</title> <title>Konulu Konum</title>
</head> </head>
<body class="container my-8"> <body class='container my-8'>
<slot /> <slot />
<SpeedInsights />
</body> </body>
</html> </html>
<style is:global></style> <style is:global></style>

View File

@ -1,16 +1,15 @@
import type { Generated } from "kysely"
export interface Database { export interface Database {
contents: ContentTable contents: ContentTable
} }
export interface ContentTable { export interface ContentTable {
id: Generated<string> id: string
url: string url: string
blob_url: string blob_url: string
loc: string loc: string
author: string author: string
description: string description: string
created_at: Generated<string> created_at: string
unlocked_counter: Generated<number> radius: number
unlocked_counter: number
} }

98
src/lib/domUtils.ts Normal file
View File

@ -0,0 +1,98 @@
import { incrementUnlockCounter } from "../components/LockedContent/serverUtils"
function updateText(elemId: string, text: string) {
const elem = document.getElementById(elemId)
if (elem) elem.innerText = text
else console.error("Element could not be found!")
}
function updateInputValue(elemId: string, value: string) {
const elem = document.getElementById(elemId) as HTMLInputElement
if (elem) elem.value = value
else console.error("Element could not be found!")
}
function toggleClass(elemId: string, className: string) {
const elem = document.getElementById(elemId)
if (elem) elem.classList.toggle(className)
else console.error("Element could not be found!")
}
function removeClasses(elemId: string, ...inputs: string[]) {
const elem = document.getElementById(elemId)
if (elem) elem.classList.remove(...inputs)
else console.error("Element could not be found!")
}
function addClasses(elemId: string, ...inputs: string[]) {
const elem = document.getElementById(elemId)
if (elem) elem.classList.add(...inputs)
else console.error("Element could not be found!")
}
function removeElement(elemId: string, silent = false) {
const elem = document.getElementById(elemId)
if (elem) elem.remove()
else if (silent) return
else console.error("Element could not be found!")
}
function addAttribute(elemId: string, attribute: string, value: string) {
const elem = document.getElementById(elemId)
if (elem) elem.setAttribute(attribute, value)
else console.error("Element could not be found!")
}
function revealContent() {
incrementUnlockCounter(document.URL.slice(-12))
removeClasses("content", "blur-2xl")
removeElement("unlock-button-container")
}
function validateFileInput(el: HTMLInputElement) {
const files = el.files
if (!files) return
if (files.length > 0) {
if (files[0].size > 4 * 1024 * 1024) {
el.setCustomValidity("Dosya boyutu 4 MB'den küçük olmalıdır.")
el.reportValidity()
} else {
el.setCustomValidity("")
}
}
}
function toggleButton(elemId: string, spinnerId: string) {
const elem = document.getElementById(elemId) as HTMLButtonElement | null
const spinner = document.getElementById(spinnerId)
if (!elem) {
throw new Error("Element could not be found!")
} else if (elem.disabled) {
spinner?.classList.add("hidden")
removeClasses(elemId, "bg-slate-500")
addClasses(elemId, "bg-slate-900")
elem.disabled = false
} else {
spinner?.classList.remove("hidden")
removeClasses(elemId, "bg-slate-900")
addClasses(elemId, "bg-slate-500")
elem.disabled = true
setTimeout(() => toggleButton(elemId, spinnerId), 1000)
}
}
export {
addClasses,
removeClasses,
removeElement,
toggleClass,
updateText,
updateInputValue,
revealContent,
addAttribute,
validateFileInput,
toggleButton,
}

View File

@ -1,31 +0,0 @@
export function onLocationError(err: GeolocationPositionError) {
let errorMessage
switch (err.code) {
case 1:
errorMessage = "Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
break
case 2:
errorMessage = "Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz."
break
case 3:
errorMessage = "Konum isteği zaman aşımına uğradı, lütfen sayfayı yenileyip tekrar deneyiniz."
break
default:
errorMessage = "Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
break
}
// @ts-ignore
Toastify({
text: errorMessage,
duration: 3000,
gravity: "top", // `top` or `bottom`
position: "center", // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: "black",
borderRadius: "6px",
margin: "16px"
},
onClick: function () {} // Callback after click
}).showToast()
}

View File

@ -1,10 +1,23 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import Toastify from "toastify-js"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function remoteLog(data: any) { export function toast(errorMessage: string) {
fetch("/api/debug", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }) Toastify({
text: errorMessage,
duration: 3000,
gravity: "top",
position: "center",
stopOnFocus: true,
style: {
background: "black",
borderRadius: "6px",
margin: "16px",
},
onClick: function () {},
}).showToast()
} }

View File

@ -1,92 +0,0 @@
---
import '@/styles/globals.css';
import '../styles/locked-page.css';
import Layout from '../layouts/Layout.astro';
import ShareButton from '../components/ShareButton';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { CalendarIcon } from '@radix-ui/react-icons';
import LockedContent from '@/components/LockedContent';
import { Separator } from '@/components/ui/separator';
import type { ContentTable } from '@/lib/db';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
type Content = Omit<ContentTable, 'url'>;
const { id } = Astro.params;
const res = await fetch(
`${import.meta.env.PUBLIC_HOME_URL}/api/content?id=${id}`
);
const data: Content | null = res.status === 200 ? await res.json() : null;
dayjs.extend(relativeTime);
dayjs.extend(utc);
// @ts-expect-error Generated<string> is string
const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc());
---
<Layout>
<main class="flex flex-col gap-4">
<div>
<h1
class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline"
>
<a href=`${import.meta.env.PUBLIC_HOME_URL}`> Konulu Konum</a>
</h1>
<p class="leading-7 [&:not(:first-child)]:mt-6">
Arkadaşınız size bir fotoğraf ve bir not bıraktı. Fakat fotoğrafı
görebilmeniz için aşağıdaki konuma gitmeniz gerekiyor. Konuma
yaklaştığınızda butona basıp fotoğrafı görüntüleyebilirsiniz.
</p>
</div>
<Separator />
<Card className="w-full">
<CardHeader>
<CardTitle>{data?.author}</CardTitle>
</CardHeader>
<CardContent>
<p>{data?.description}</p>
</CardContent>
<CardFooter className="gap-2">
<CalendarIcon />
<p>{dateFromNow}</p>
</CardFooter>
</Card>
<LockedContent
contentId={data?.id}
imageUrl={data?.blob_url}
location={data?.loc}
client:load
/>
<div
id="map"
class="w-full h-[450px] rounded"
data-target-location={data?.loc}
>
</div>
<ShareButton client:only />
<div class="flex justify-center">
<p class="text-muted-foreground">
Fotoğrafın kilidi şu ana dek <b>{data?.unlocked_counter}</b> kez açıldı
</p>
</div>
</main>
<script src="../scripts/initMap.js"></script>
</Layout>

View File

@ -1,39 +0,0 @@
import type { Database } from "@/lib/db"
import { createKysely } from "@vercel/postgres-kysely"
import type { APIRoute } from "astro"
export const GET: APIRoute = async ({ request }) => {
const contentId = new URL(request.url).searchParams.get("id")
if (!contentId) {
return new Response(null, {
status: 400,
statusText: "Content id is required"
})
}
const db = createKysely<Database>({ connectionString: import.meta.env.POSTGRES_URL })
try {
const result = await db
.updateTable("contents")
.set(eb => ({ unlocked_counter: eb("unlocked_counter", "+", 1) }))
.where("id", "=", contentId)
.executeTakeFirst()
if (result) {
return new Response(JSON.stringify({ counter: Number(result) }))
} else {
return new Response(null, {
status: 404,
statusText: "Could not increment the counter"
})
}
} catch (error) {
console.error("Error fetching content:", error)
return new Response(null, {
status: 500,
statusText: "Error while fetching content"
})
}
}

View File

@ -1,141 +0,0 @@
import { createClient } from "@supabase/supabase-js"
import type { APIRoute } from "astro"
import { createKysely } from "@vercel/postgres-kysely"
import { customAlphabet } from "nanoid"
import sharpService from "astro/assets/services/sharp"
import type { Database } from "@/lib/db"
export const POST: APIRoute = async ({ request }) => {
const formData = await request.formData()
const image = formData.get("selected-photo") as File
const author = formData.get("author")
const description = formData.get("description")
const geolocation = formData.get("geolocation")
if (!image || !geolocation) {
return new Response(null, {
status: 400,
statusText: "Image and geolocation are required fields"
})
}
const pos = geolocation.toString().split(",")
if (pos.length !== 2) {
return new Response(null, {
status: 400,
statusText: "Geolocation not correctly formatted"
})
}
const supabaseUrl = import.meta.env.SUPABASE_URL
const supabaseKey = import.meta.env.SUPABASE_KEY
const supabase = createClient(supabaseUrl, supabaseKey)
const nanoid = customAlphabet("abcdefghijklmnoprstuvyz", 10)
const randomImageId = nanoid()
const imageName = `${image.name.replace(/\.[^/.]+$/, "")}${randomImageId}.webp`
const imageBuf = await image.arrayBuffer()
const { data, format } = await sharpService.transform(
new Uint8Array(imageBuf),
{ src: imageName, format: "webp" },
{ domains: [], remotePatterns: [], service: { entrypoint: "", config: { limitInputPixels: false } } }
)
console.log(format)
const { error } = await supabase.storage.from("images").upload(`public/${imageName}`, data, {
cacheControl: "3600",
upsert: false,
contentType: "image/webp"
})
if (error) {
console.error(error.message, imageName, error.cause)
return new Response(null, {
status: 400,
statusText: error.message
})
}
const imagePublicUrl = `https://sozfqjbdyppxfwhqktja.supabase.co/storage/v1/object/public/images/public/${imageName}`
const db = createKysely<Database>({ connectionString: import.meta.env.POSTGRES_URL })
const newUrl = nanoid()
const res = await db
.insertInto("contents")
.values({
url: `${newUrl.slice(0, 3)}-${newUrl.slice(3, 7)}-${newUrl.slice(7)}`,
blob_url: imagePublicUrl,
author: author?.toString() ?? "",
description: description?.toString() ?? "",
loc: `SRID=4326;POINT(${pos[0]} ${pos[1]})`
})
.returning("url")
.executeTakeFirst()
if (res?.url) {
return new Response(
JSON.stringify({
url: res.url
})
)
} else {
return new Response(null, {
status: 500,
statusText: "Error while saving data"
})
}
}
export const GET: APIRoute = async ({ request }) => {
const contentId = new URL(request.url).searchParams.get("id")
if (!contentId) {
return new Response(null, {
status: 400,
statusText: "Content id is required"
})
}
const db = createKysely<Database>({ connectionString: import.meta.env.POSTGRES_URL })
try {
const content = await db
.selectFrom("contents")
.select(({ fn }) => [
"id",
"blob_url",
fn<string>("ST_AsGeoJSON", ["loc"]).as("loc"),
"description",
"author",
"created_at",
"unlocked_counter"
])
.where("url", "=", contentId)
.executeTakeFirst()
if (content) {
return new Response(JSON.stringify(content))
} else {
return new Response(null, {
status: 404,
statusText: "Content not found"
})
}
} catch (error) {
console.error("Error fetching content:", error)
return new Response(null, {
status: 500,
statusText: "Error while fetching content"
})
}
}

View File

@ -1,11 +0,0 @@
import type { APIRoute } from "astro"
export const POST: APIRoute = async ({ request }) => {
const data = await request.json()
console.log(data)
return new Response(null, {
status: 200
})
}

View File

@ -1,56 +1,50 @@
--- ---
export const prerender = true; import "@/styles/globals.css"
import "../styles/locked-page.css"
import '@/styles/globals.css'; import Layout from "../layouts/Layout.astro"
import Layout from '../layouts/Layout.astro'; import { Loader2 } from "lucide-react"
import { Button } from '@/components/ui/button';
import '../styles/locked-page.css'; const backendUrl = import.meta.env.PUBLIC_BACKEND_URL
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { ReloadIcon } from '@radix-ui/react-icons';
import { Label } from '@/components/ui/label';
--- ---
<Layout> <Layout>
<main> <main>
<form <form
class="flex flex-col gap-8" class='flex flex-col gap-12 md:gap-10'
id="sample-form" id='sample-form'
enctype="multipart/form-data" action={`${backendUrl}/api/location`}
> method='post'
enctype='multipart/form-data'>
<div> <div>
<h1 <h1
class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline" class='scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline'>
> <a href=`${import.meta.env.SITE}`> Konulu Konum</a>
<a href=`${import.meta.env.PUBLIC_HOME_URL}`> Konulu Konum</a>
</h1> </h1>
<p class="leading-7 [&:not(:first-child)]:mt-6"> <p class='leading-7 [&:not(:first-child)]:mt-6 text-2xl'>
3 basit adımda fotoğraflarınızı ve videolarınızı <b 3 basit adımda fotoğraflarınızı ve videolarınızı <b
>yalnızca belirli bir konumda</b >yalnızca belirli bir konumda</b
> açılacak şekilde kilitleyin: > açılacak şekilde kilitleyin.
</p> </p>
</div> </div>
<div> <div>
<h2 <h2
class="mt-10 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0" class='mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0'>
>
1. Fotoğraf Seçimi 1. Fotoğraf Seçimi
</h2> </h2>
<p class="leading-7 [&:not(:first-child)]:mt-6"> <p class='leading-7 [&:not(:first-child)]:mt-6 text-2xl'>
Aşağıdaki butona basıp galerinizden bir fotoğraf seçin, seçtiğiniz Bir fotoğraf seçin.
fotoğraf yalnızca belirli bir konumda açılabilecek bir hale
getirilecek.
</p> </p>
<div class="grid w-full max-w-sm items-center gap-1.5 mt-4"> <div class='grid items-center gap-1.5 mt-4'>
<Input <input
type="file" type='file'
name="selected-photo" name='selected-photo'
id="photo-selector" id='photo-selector'
class='flex w-full p-2 border border-gray rounded-lg file:bg-inherit file:border-0'
required required
/> />
<p class="text-sm text-muted-foreground"> <p class='px-2 text-lg text-muted-foreground'>
Galerinizden bir fotoğraf seçin. Galerinizden bir fotoğraf seçin.
</p> </p>
</div> </div>
@ -58,173 +52,94 @@ import { Label } from '@/components/ui/label';
<div> <div>
<h2 <h2
class="mt-10 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0" class='mt-10 scroll-m-20 border-b pb-2 font-semibold tracking-tight transition-colors first:mt-0 text-3xl'>
>
2. Fotoğrafın Açılacağı Konum 2. Fotoğrafın Açılacağı Konum
</h2> </h2>
<p class="leading-7 [&:not(:first-child)]:mt-6"> <p class='leading-7 [&:not(:first-child)]:mt-6 text-2xl'>
Haritadan bir nokta seçin. Bağlantıyı gönderdiğiniz kişi bu konuma Haritadan bir nokta seçin.
gittiği zaman seçtiğiniz fotoğrafı görebilecek.
</p> </p>
<div> <div>
<div <div
id="map" id='map'
class="w-full h-[450px] rounded mt-4 transition-colors ease-in-out border-2 duration-300" class='w-full h-[450px] rounded mt-6 transition-colors ease-in-out border-2 duration-300'>
>
</div> </div>
<p class="text-xl mt-2"> <p class='text-lg text-muted-foreground mt-2'>
<span class="font-semibold">Seçilen Konum:</span> Bağlantıyı gönderdiğiniz kişi bu konuma gittiği zaman seçtiğiniz
fotoğrafı görebilecek.
</p>
<p class='text-2xl mt-4 mb-2'>
<span class='font-semibold'>Seçilen Konum:</span>
</p> </p>
<p <p
class="text-lg transition ease-in-out duration-700" class='text-xl transition ease-in-out duration-700'
id="coordinates" id='coordinates'>
>
Henüz konum seçmediniz, konum seçmek için haritadaki bir noktaya Henüz konum seçmediniz, konum seçmek için haritadaki bir noktaya
basınız. basınız.
</p> </p>
<p <p
class="text-lg text-muted-foreground mt-2" class='text-lg text-muted-foreground mt-2'
id="location-selected-confirmation" id='location-selected-confirmation'>
>
</p> </p>
</div> </div>
<input name="geolocation" id="geolocation-input" hidden /> <input name='geolocation' id='geolocation-input' hidden />
<input name='geolocation-radius' id='geolocation-radius-input' hidden />
</div> </div>
<div> <div>
<h2 <h2
class="mt-10 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0" class='mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0'>
>
3. Fotoğraf Açıklaması 3. Fotoğraf Açıklaması
</h2> </h2>
<div class="grid w-full max-w-md items-center gap-2 mt-4"> <div class='grid w-full max-w-md items-center gap-4 mt-4'>
<div class="grid w-full items-center gap-1.5"> <div class='grid w-full items-center gap-2'>
<Label htmlFor="location-author" className="lg:text-lg text-md"> <label for='location-author' class='text-xl'> Gönderici: </label>
Gönderici <input
</Label> id='location-author'
<Input name='author'
id="location-author" placeholder='İsminizi buraya yazınız.'
name="author" class='text-lg py-2 px-3 border border-gray rounded-lg placeholder:text-slate-400'
placeholder="İsminizi buraya yazınız."
className="lg:text-lg text-md"
required required
/> />
</div> </div>
<div class="grid w-full items-center gap-1.5"> <div class='grid w-full items-center gap-2'>
<Label <label for='location-description' class='text-xl'>
htmlFor="location-description" ıklama:
className="lg:text-lg text-md" </label>
> <textarea
ıklama placeholder='Açıklamanızı buraya yazınız.'
</Label> name='description'
<Textarea id='location-description'
placeholder="Açıklamanızı buraya yazınız." class='text-lg py-2 px-3 border border-gray rounded-lg placeholder:text-slate-400 resize-none'
name="description" required></textarea>
id="location-description" <p class='text-lg text-muted-foreground'>
className="lg:text-lg text-md"
required
/>
<p class="text-sm text-muted-foreground">
Bir açıklama girin, yüklediğiniz içeriğin üzerine bu metin Bir açıklama girin, yüklediğiniz içeriğin üzerine bu metin
görünecek. görünecek.
</p> </p>
</div> </div>
</div> </div>
<div class="flex gap-2 my-6 items-center"> <hr class='my-4' />
<input type="checkbox" id="kvkk" class="w-6 h-6" required /> <div class='flex gap-2 my-6 items-center mt-4'>
<Label <input type='checkbox' id='kvkk' class='w-6 h-6' required />
htmlFor="kvkk" <label
className="text-lg font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" for='kvkk'
> class='text-xl font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
Konulu Konum'u KVKK kapsamında dava etmeyeceğimi onaylıyorum. Konulu Konum'u KVKK kapsamında dava etmeyeceğimi onaylıyorum.
</Label> </label>
</div> </div>
<Button <button
className="w-full text-lg" class='w-full text-lg bg-slate-900 text-white p-2 rounded-lg inline-flex justify-center items-center gap-2'
type="submit" type='submit'
id="submit-button" id='submit-button'>
size="lg" <Loader2 id='submit-button-spinner' className='animate-spin hidden' />
>
<ReloadIcon
className="mr-2 h-4 w-4 animate-spin hidden"
id="submit-button-reload"
/>
Bağlantıyı Oluştur Bağlantıyı Oluştur
</Button> </button>
</div> </div>
</form> </form>
<script src="../scripts/initSelectionMap.js"></script> <script src='../scripts/initSelectionMap.ts'></script>
<script src='../scripts/index.ts'></script>
</main> </main>
<script>
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const locationSelected = document.getElementById(
'location-selected-confirmation'
)?.innerText;
if (!locationSelected) {
const coordinatesText = document.getElementById('coordinates');
const mapDiv = document.getElementById('map');
mapDiv?.classList.add('border-slate-700');
coordinatesText?.classList.add('drop-shadow-xl');
setTimeout(() => {
mapDiv?.classList.remove('border-slate-700');
coordinatesText?.classList.remove('drop-shadow-xl');
}, 500);
return;
}
const submitButton = document.getElementById(
'submit-button'
) as HTMLButtonElement;
const reloadIcon = document.getElementById(
'submit-button-reload'
) as HTMLElement;
submitButton.disabled = true;
reloadIcon.classList.toggle('hidden');
const formData = new FormData(e.target as HTMLFormElement);
const res = await fetch(`/api/content`, {
method: 'POST',
body: formData,
});
reloadIcon.classList.toggle('hidden');
submitButton.disabled = false;
if (res.status === 200) {
const data = await res.json();
if (data.url) location.assign('/' + data.url);
} else {
// @ts-ignore
Toastify({
text: 'Konulu konum oluşturulamadı, lütfen tekrar deneyin.',
duration: 3000,
gravity: 'top', // `top` or `bottom`
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: 'black',
borderRadius: '6px',
margin: '16px',
},
onClick: function () {}, // Callback after click
}).showToast();
}
};
document.getElementById('sample-form')!.onsubmit = handleSubmit;
</script>
</Layout> </Layout>

170
src/pages/x/index.astro Normal file
View File

@ -0,0 +1,170 @@
---
// Styles
import "@/styles/globals.css"
import "@/styles/locked-page.css"
import "@/styles/locked-content.css"
// Components
import Layout from "@/layouts/Layout.astro"
import ShareButton from "@/components/ShareButton"
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { CalendarIcon } from "@radix-ui/react-icons"
---
<Layout>
<main class='flex flex-col gap-4'>
<div class='flex flex-col'>
<h1
class='scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline'>
<a href=`${import.meta.env.SITE}`> Konulu Konum</a>
</h1>
<p class='[&:not(:first-child)]:mt-6 text-2xl'>
Size bir konulu konum bırakıldı, görüntüleyebilmek için haritadaki
konuma gitmeniz gerekiyor.
</p>
</div>
<Card className='w-full'>
<CardHeader>
<CardTitle className='text-2xl' id='card-title' />
</CardHeader>
<CardContent className='text-xl'>
<p id='card-description'></p>
</CardContent>
<CardFooter className='gap-2 text-lg'>
<CalendarIcon />
<p id='card-footer'></p>
</CardFooter>
</Card>
<div
id='locked-content-container'
class='w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded'>
<div class='flex flex-col justify-center items-center image-wrapper'>
<img
id='content'
src='#'
class='h-[450px] blur-2xl transition-all duration-1000'
/>
<div
id='unlock-button-container'
class='flex flex-col justify-center gap-4 overlay'>
<button
id='unlock-content-button'
class='transition-[background-color] duration-1000 inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-11 rounded-md text-lg p-6 text-md'>
<svg
id='lock-icon'
xmlns='http://www.w3.org/2000/svg'
width='28'
height='28'
fill='#ffffff'
viewBox='0 0 256 256'>
<path
d='M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z'
></path>
</svg>
<svg
id='unlock-icon'
class='hidden'
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
fill='#ffffff'
viewBox='0 0 256 256'>
<path
d='M208,80H96V56a32,32,0,0,1,32-32c15.37,0,29.2,11,32.16,25.59a8,8,0,0,0,15.68-3.18C171.32,24.15,151.2,8,128,8A48.05,48.05,0,0,0,80,56V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm0,128H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z'
></path>
</svg>
<p id='button-text' class='text-xl'>İçerik Kilitli</p>
</button>
<div
class='rounded-lg border bg-card text-card-foreground shadow-sm p-2'>
<div class='pb-0 text-center flex flex-col gap-4'>
<p id='locked-content-description' class='text-xl'>
Ne kadar yaklaştığını görmek için aşağıdaki butona bas.
</p>
<button
id='location-permission-button'
class='text-xl inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 text-primary-foreground h-10 rounded-md px-3 bg-green-700 hover:bg-green-600 text-md'>
Konum İzni Ver
</button>
</div>
</div>
</div>
</div>
</div>
<div id='map' class='w-full h-[450px] rounded'></div>
<ShareButton client:only />
<div class='flex justify-center'>
<p class='text-muted-foreground'>
Fotoğrafın kilidi şu ana dek <b id='counter'></b> kez açıldı
</p>
</div>
</main>
<script>
import type { ContentTable } from "@/lib/db"
import { updateText } from "@/lib/domUtils"
// Dayjs
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import utc from "dayjs/plugin/utc"
// Map
import { initMap } from "@/scripts/initMap"
const url = new URL(document.URL).searchParams
const id = url.get("id")
type Content = ContentTable
const res = await fetch(
`${import.meta.env.PUBLIC_BACKEND_URL}/api/location/${id}`
)
const data: Content | null = res.status === 200 ? await res.json() : null
dayjs.extend(relativeTime)
dayjs.extend(utc)
const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc())
updateText("card-title", data?.author ?? "")
updateText("card-description", data?.description ?? "")
updateText("card-footer", dateFromNow)
const lockedContentContainer = document.getElementById(
"locked-content-container"
)
if (lockedContentContainer) {
lockedContentContainer.dataset.targetPosition = data?.loc
}
const leafletMap = document.getElementById("map")
if (leafletMap) {
leafletMap.dataset.targetLocation = data?.loc
leafletMap.dataset.targetRadius = data?.radius.toString() ?? "50"
}
initMap()
const content = document.getElementById(
"content"
) as HTMLImageElement | null
if (content)
content.src = `${import.meta.env.PUBLIC_BACKEND_URL}/images/${data?.blob_url}`
const counter = document.getElementById("counter")
if (counter) counter.innerText = data?.unlocked_counter.toString() ?? ""
</script>
<script src='@/scripts/initMap.ts'></script>
<script src='@/scripts/lockedContent.ts'></script>
</Layout>

53
src/scripts/index.ts Normal file
View File

@ -0,0 +1,53 @@
import { toggleButton, validateFileInput } from "@/lib/domUtils"
import { toast } from "@/lib/utils"
export function toggleMap() {
const coordinatesText = document.getElementById("coordinates")
const mapDiv = document.getElementById("map")
mapDiv?.classList.add("border-slate-900")
coordinatesText?.classList.add("drop-shadow-2xl")
setTimeout(() => {
mapDiv?.classList.remove("border-slate-900")
coordinatesText?.classList.remove("drop-shadow-2xl")
}, 800)
}
const handleSubmit = async (e: SubmitEvent) => {
toggleButton("submit-button", "submit-button-spinner")
const locationSelected = document.getElementById(
"geolocation-input"
) as HTMLInputElement | null
if (!locationSelected) {
throw new Error("Element could not be found!")
}
if (!locationSelected.value) {
const map = document.getElementById("map")
map?.scrollIntoView({ behavior: "smooth", block: "center" })
return toggleMap()
}
const inputEl = document.getElementById(
"photo-selector"
) as HTMLInputElement | null
if (!inputEl) {
throw new Error("Element could not be found!")
}
validateFileInput(inputEl)
}
document.getElementById("sample-form")!.onsubmit = handleSubmit
document.getElementById("photo-selector")!.oninput = (ev) =>
validateFileInput(ev.target as HTMLInputElement)
const url = new URL(document.URL)
if (url.searchParams.get("error"))
toast("Konulu konum oluşturulamadı, lütfen tekrar deneyin.")

View File

@ -1,161 +0,0 @@
const data = JSON.parse(document.getElementById('map').dataset.targetLocation)
const TARGET_LOCATION = data.coordinates
function startWatchingLocation() {
map.locate({ watch: true })
}
var map = L.map('map').setView(TARGET_LOCATION, 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
var targetLocationIcon = L.icon({
iconUrl: 'goal.svg',
iconSize: [32, 32],
});
L.marker(TARGET_LOCATION, { icon: targetLocationIcon }).addTo(map);
var circle = L.circle(TARGET_LOCATION, {
color: 'blue',
fillColor: '#30f',
fillOpacity: 0.2,
radius: 50
}).addTo(map);
var currentLocationIcon = L.icon({
iconUrl: 'blue-dot.png',
iconSize: [32, 32],
});
let currentLocationMarker;
function onLocationError(err) {
let errorMessage;
switch (err.code) {
case 1:
errorMessage = 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.'
break;
case 2:
errorMessage = 'Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz.'
break;
case 3:
errorMessage = 'Konum isteği zaman aşımına uğradı, lütfen sayfayı yenileyip tekrar deneyiniz.'
break;
default:
errorMessage = 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.'
break;
}
// @ts-ignore
Toastify({
text: errorMessage,
duration: 3000,
gravity: 'top', // `top` or `bottom`
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: 'black',
borderRadius: '6px',
margin: '16px',
},
onClick: function () { }, // Callback after click
}).showToast();
}
function onLocationSuccess(locationEvent) {
const position = locationEvent.latlng
const currentPos = {
lat: position.lat,
lng: position.lng
}
if (currentLocationMarker) {
currentLocationMarker.setLatLng(currentPos);
} else {
currentLocationMarker = L.marker(currentPos, { icon: currentLocationIcon });
currentLocationMarker.addTo(map);
}
}
map.on('locationerror', onLocationError);
map.on('locationfound', onLocationSuccess)
L.Control.GoToCurrentLocation = L.Control.extend({
onAdd: function (map) {
const locationButton = document.createElement('button');
locationButton.textContent = 'Konum İzni Ver';
locationButton.classList.add('custom-map-control-button');
locationButton.type = 'button'
locationButton.addEventListener('click', (ev) => {
if (locationButton.textContent != 'Konumuma Git') {
startWatchingLocation()
locationButton.textContent = 'Konumuma Git';
} else {
if (currentLocationMarker) {
map.setView(currentLocationMarker.getLatLng(), 12);
} else {
// @ts-ignore
Toastify({
text: 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.',
duration: 3000,
gravity: 'top', // `top` or `bottom`
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: 'black',
borderRadius: '6px',
margin: '16px',
},
onClick: function () { }, // Callback after click
}).showToast();
}
}
L.DomEvent.stopPropagation(ev)
})
return locationButton;
},
});
L.Control.GoToTargetLocation = L.Control.extend({
onAdd: function (map) {
const locationButton = document.createElement('button');
locationButton.textContent = 'Hedefe Git';
locationButton.classList.add('custom-map-control-button');
locationButton.addEventListener('click', () => {
map.setView(TARGET_LOCATION, 18);
});
return locationButton;
}
});
L.control.currentLocation = function (opts) {
return new L.Control.GoToCurrentLocation(opts);
};
L.control.targetLocation = function (opts) {
return new L.Control.GoToTargetLocation(opts);
};
L.control.currentLocation({ position: 'bottomleft' }).addTo(map);
L.control.targetLocation({ position: 'bottomleft' }).addTo(map);

120
src/scripts/initMap.ts Normal file
View File

@ -0,0 +1,120 @@
import {
currentLocationIcon,
targetLocationIcon,
} from "@/components/Leaflet/icons"
import { toast } from "@/lib/utils"
import L from "leaflet"
var map: L.Map
const mapEl = document.getElementById("map")
type TargetLocation = [lat: number, lng: number] | null
let currentLocationMarker: L.Marker
export function mapLocationSuccessCallback(position: GeolocationPosition) {
const currentPos = {
lat: position.coords.latitude,
lng: position.coords.longitude,
}
if (currentLocationMarker) {
currentLocationMarker.setLatLng(currentPos)
} else {
currentLocationMarker = L.marker(currentPos, { icon: currentLocationIcon })
currentLocationMarker.addTo(map)
}
}
const CurrentLocation = L.Control.extend({
onAdd: function (map: L.Map) {
const locationButton = document.createElement("button")
locationButton.textContent = "Konumuma Git"
locationButton.classList.add("custom-map-control-button", "disabled-button")
locationButton.type = "button"
locationButton.id = "current-location-control"
locationButton.addEventListener("click", () => {
if (currentLocationMarker) {
map.setView(currentLocationMarker.getLatLng(), 12)
} else {
toast("Konumunuza erişilemiyor, lütfen biraz bekleyip tekrar deneyin.")
}
})
return locationButton
},
})
const GoToTargetLocation = L.Control.extend({
onAdd: function (map: L.Map) {
const locationButton = document.createElement("button")
locationButton.textContent = "Hedefe Git"
locationButton.classList.add("custom-map-control-button")
locationButton.addEventListener("click", () => {
const targetLocation = getTargetLocation()
if (targetLocation) map.setView(targetLocation, 12)
})
return locationButton
},
})
const goToCurrentLocationControl = new CurrentLocation({
position: "bottomleft",
})
const targetLocationControl = new GoToTargetLocation({
position: "bottomleft",
})
function addTargetLocationMarker(target: TargetLocation, radius = 50) {
if (target) {
L.marker(target, { icon: targetLocationIcon }).addTo(map)
L.circle(target, {
color: "blue",
fillColor: "#30f",
fillOpacity: 0.2,
radius,
}).addTo(map)
}
}
function getTargetLocation(): TargetLocation | null {
const targetLocation = mapEl?.dataset.targetLocation
const data = targetLocation ? JSON.parse(targetLocation) : null
return data
}
export function initMap() {
map = L.map("map")
const targetLocation = mapEl?.dataset.targetLocation
const targetRadius = mapEl?.dataset.targetRadius
const data = targetLocation ? JSON.parse(targetLocation) : null
const TARGET_LOCATION = data as TargetLocation
if (TARGET_LOCATION) map.setView(TARGET_LOCATION, 13)
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map)
addTargetLocationMarker(TARGET_LOCATION, Number(targetRadius))
targetLocationControl.addTo(map)
goToCurrentLocationControl.addTo(map)
}

View File

@ -1,119 +0,0 @@
import { onLocationError } from "@/lib/error";
var map = L.map('map').setView([41.024857599001905, 28.940787550837882], 10);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
let targetLocationMarker;
let targetLocationCircle;
var targetLocationIcon = L.icon({
iconUrl: 'goal.svg',
iconSize: [32, 32],
});
var currentLocationIcon = L.icon({
iconUrl: 'blue-dot.png',
iconSize: [32, 32],
});
let currentLocationMarker;
function startWatchingLocation() {
map.locate({ watch: true })
}
function onLocationSuccess(locationEvent) {
const position = locationEvent.latlng
const currentPos = {
lat: position.lat,
lng: position.lng
}
if (currentLocationMarker) {
currentLocationMarker.setLatLng(currentPos);
} else {
currentLocationMarker = L.marker(currentPos, { icon: currentLocationIcon });
currentLocationMarker.addTo(map);
}
}
map.on('locationerror', onLocationError);
map.on('locationfound', onLocationSuccess)
L.Control.GoToCurrentLocation = L.Control.extend({
onAdd: function (map) {
const locationButton = document.createElement('button');
locationButton.textContent = 'Konum İzni Ver';
locationButton.classList.add('custom-map-control-button');
locationButton.type = 'button'
locationButton.addEventListener('click', (ev) => {
if (locationButton.textContent != 'Konumuma Git') {
startWatchingLocation()
locationButton.textContent = 'Konumuma Git';
} else {
if (currentLocationMarker) {
map.setView(currentLocationMarker.getLatLng(), 12);
} else {
// @ts-ignore
Toastify({
text: 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.',
duration: 3000,
gravity: 'top', // `top` or `bottom`
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: 'black',
borderRadius: '6px',
margin: '16px',
},
onClick: function () { }, // Callback after click
}).showToast();
}
}
L.DomEvent.stopPropagation(ev)
})
return locationButton;
},
});
L.control.currentLocation = function (opts) {
return new L.Control.GoToCurrentLocation(opts);
};
L.control.currentLocation({ position: 'bottomleft' }).addTo(map);
map.on('click', (e) => {
if (targetLocationMarker) {
targetLocationMarker.setLatLng(e.latlng)
targetLocationCircle.setLatLng(e.latlng)
} else {
targetLocationMarker = L.marker(e.latlng, { icon: targetLocationIcon }).addTo(map);
targetLocationCircle = L.circle(e.latlng, {
color: 'blue',
fillColor: '#30f',
fillOpacity: 0.2,
radius: 50
}).addTo(map);
}
const pos = targetLocationMarker.getLatLng()
document.getElementById('coordinates').innerText = `${pos.lat}. Enlem, ${pos.lng}. Boylam`
document.getElementById('geolocation-input').value = `${pos.lat},${pos.lng}`
document.getElementById('location-selected-confirmation').innerText = "Konum seçildi, bir sonraki adıma geçebilirsiniz."
})

View File

@ -0,0 +1,240 @@
import L from "leaflet"
import { openstreetmapTiles } from "@/components/Leaflet/constants"
import { toast } from "@/lib/utils"
import {
currentLocationIcon,
targetLocationIcon,
} from "@/components/Leaflet/icons"
import {
addClasses,
removeClasses,
updateInputValue,
updateText,
} from "@/lib/domUtils"
import { updateMarkerLocation } from "@/components/Leaflet/geolocation"
var map = L.map("map").setView([41.024857599001905, 28.940787550837882], 10)
openstreetmapTiles.addTo(map)
let targetLocationMarker: L.Marker
let targetLocationCircle: L.Circle
let targetLocationCircleRadius = 50
let currentLocationMarker: L.Marker
map.on("locationerror", () =>
toast(
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
)
)
map.on("locationfound", (ev) => {
removeClasses("current-location-control", "disabled-button")
addClasses("ask-permission-control", "disabled-button")
currentLocationMarker = updateMarkerLocation(
currentLocationMarker,
currentLocationIcon,
ev.latlng,
map
)
})
const CurrentLocationControl = L.Control.extend({
onAdd: function (map: L.Map) {
const locationButton = document.createElement("button")
locationButton.textContent = "Konumuma Git"
locationButton.classList.add("custom-map-control-button", "disabled-button")
locationButton.type = "button"
locationButton.id = "current-location-control"
locationButton.addEventListener("click", (ev) => {
if (currentLocationMarker) {
map.setView(currentLocationMarker.getLatLng(), 12)
} else {
toast("Konumunuza erişilemiyor, lütfen biraz bekleyip tekrar deneyin.")
}
L.DomEvent.stopPropagation(ev)
})
return locationButton
},
})
const AskPermissonControl = L.Control.extend({
onAdd: function (map: L.Map) {
const locationButton = document.createElement("button")
locationButton.textContent = "Konum İzni Ver"
locationButton.classList.add("custom-map-control-button")
locationButton.type = "button"
locationButton.id = "ask-permission-control"
locationButton.addEventListener("click", (ev) => {
map.locate({ watch: true })
L.DomEvent.stopPropagation(ev)
})
return locationButton
},
})
const RadiusControl = L.Control.extend({
onAdd: function (map: L.Map) {
const radiusContainer = document.createElement("div")
radiusContainer.classList.add(
"bg-white",
"h-[40px]",
"p-2",
"flex",
"items-center",
"gap-2"
)
const radiusButtonClasses = [
"text-xl",
"border-2",
"border-slate-700",
"max-h-[30px]",
"flex",
"items-center",
"p-2",
"rounded-lg",
"bg-gray-100",
"hover:bg-gray-300",
]
const radiusIncreaseButton = document.createElement("button")
radiusIncreaseButton.classList.add(...radiusButtonClasses)
const radiusDecreaseButton = document.createElement("button")
radiusIncreaseButton.type = "button"
radiusDecreaseButton.type = "button"
radiusDecreaseButton.classList.add(...radiusButtonClasses)
const radiusContainerText = document.createElement("p")
const radiusText = document.createElement("p")
radiusContainerText.classList.add("text-xl")
radiusText.classList.add("text-xl")
radiusIncreaseButton.textContent = "+"
radiusDecreaseButton.textContent = "-"
radiusContainerText.textContent = "Yarıçap: "
radiusText.textContent = `${targetLocationCircleRadius.toString()}m`
radiusContainer.insertAdjacentElement("afterbegin", radiusIncreaseButton)
radiusContainer.insertAdjacentElement("afterbegin", radiusText)
radiusContainer.insertAdjacentElement("afterbegin", radiusDecreaseButton)
radiusContainer.insertAdjacentElement("afterbegin", radiusContainerText)
radiusContainer.id = "radius-control"
L.DomEvent.on(radiusIncreaseButton, "click", (ev) => {
targetLocationCircleRadius = Math.min(
targetLocationCircleRadius + 100,
2000
)
targetLocationCircle.setRadius(targetLocationCircleRadius)
radiusText.innerText = `${targetLocationCircleRadius.toString()}m`
updateInputValue(
"geolocation-radius-input",
targetLocationCircleRadius.toString()
)
L.DomEvent.stop(ev)
})
L.DomEvent.on(radiusIncreaseButton, "dblclick", (ev) => L.DomEvent.stop(ev))
L.DomEvent.on(radiusDecreaseButton, "click", (ev) => {
targetLocationCircleRadius = Math.max(
targetLocationCircleRadius - 100,
50
)
targetLocationCircle.setRadius(targetLocationCircleRadius)
radiusText.innerText = `${targetLocationCircleRadius.toString()}m`
updateInputValue(
"geolocation-radius-input",
targetLocationCircleRadius.toString()
)
L.DomEvent.stop(ev)
})
L.DomEvent.on(radiusDecreaseButton, "dblclick", (ev) => L.DomEvent.stop(ev))
return radiusContainer
},
})
const askPermissionControl = new AskPermissonControl({ position: "bottomleft" })
const currentLocationControl = new CurrentLocationControl({
position: "bottomleft",
})
const radiusControl = new RadiusControl({ position: "topright" })
askPermissionControl.addTo(map)
currentLocationControl.addTo(map)
map.on("click", (e) => {
if (targetLocationMarker) {
targetLocationMarker.setLatLng(e.latlng)
targetLocationCircle.setLatLng(e.latlng)
} else {
targetLocationMarker = L.marker(e.latlng, {
icon: targetLocationIcon,
}).addTo(map)
targetLocationCircle = L.circle(e.latlng, {
color: "blue",
fillColor: "#30f",
fillOpacity: 0.2,
radius: targetLocationCircleRadius,
}).addTo(map)
radiusControl.addTo(map)
}
const pos = targetLocationMarker.getLatLng()
updateInputValue("geolocation-input", `${pos.lat},${pos.lng}`)
updateText("coordinates", `${pos.lat}. Enlem, ${pos.lng}. Boylam`)
updateText(
"location-selected-confirmation",
"Konum seçildi, bir sonraki adıma geçebilirsiniz."
)
})
const geolocationInputEl = document.getElementById(
"geolocation-input"
) as HTMLInputElement | null
if (geolocationInputEl?.value) {
geolocationInputEl.value = ""
}

View File

@ -0,0 +1,94 @@
import {
errorCallback,
locationSuccessCallback,
} from "@/components/LockedContent/geolocation"
import type { LatLngTuple } from "leaflet"
// Geolocation watch id to avoid duplicate watchPosition calls
let watchId: number
// DOM ELEMENTS
const locationPermissionButton = document.getElementById(
"location-permission-button"
)
const unlockButton = document.getElementById("unlock-content-button")
const lockedContentContainer = document.getElementById(
"locked-content-container"
)
// These elements MUST be defined
// Throw error if they are not found
if (!locationPermissionButton || !lockedContentContainer || !unlockButton) {
throw new Error("Element not found!")
}
// EVENT LISTENERS
locationPermissionButton.addEventListener("click", startWatchingLocation)
lockedContentContainer.addEventListener("askpermission", startWatchingLocation)
// Get target position from container's dataset
function getTargetPosition() {
const lockedContentContainer = document.getElementById(
"locked-content-container"
)
const targetPositionString = lockedContentContainer?.dataset.targetPosition
// TARGET_POSITION is required to calculate distance
if (!targetPositionString) throw new Error("Target position is not provided!")
const data = JSON.parse(targetPositionString) as LatLngTuple
return data
}
function getRadius() {
const leafletMap = document.getElementById("map")
let targetRadiusString = leafletMap?.dataset.targetRadius
// TARGET_POSITION is required to calculate distance
if (!targetRadiusString) targetRadiusString = "50"
const data = Number(targetRadiusString)
return data
}
// Call Geolocation API to start watching user location
function startWatchingLocation() {
const TARGET_POSITION = getTargetPosition()
const radius = getRadius()
if (!watchId) {
watchId = window.navigator.geolocation.watchPosition(
(position) => locationSuccessCallback(position, TARGET_POSITION, radius),
errorCallback
)
}
}
// When the web page is loaded, check if user has given geolocation
// permission before
navigator.permissions
.query({ name: "geolocation" })
.then((permissionStatus) => {
switch (permissionStatus.state) {
case "granted":
const TARGET_POSITION = getTargetPosition()
const radius = getRadius()
watchId = window.navigator.geolocation.watchPosition(
(position) =>
locationSuccessCallback(position, TARGET_POSITION, radius),
errorCallback
)
break
case "denied":
case "prompt":
default:
break
}
})

View File

@ -1,11 +1,24 @@
.custom-map-control-button { .custom-map-control-button {
@apply text-xl;
background-color: #fff;
border: 0;
border-radius: 2px;
box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
margin: 10px;
padding: 0 0.5em;
overflow: hidden;
height: 40px;
cursor: pointer;
}
.custom-map-control-div {
@apply text-xl;
background-color: #fff; background-color: #fff;
border: 0; border: 0;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
margin: 10px; margin: 10px;
padding: 0 0.5em; padding: 0 0.5em;
font: 400 18px Roboto, Arial, sans-serif;
overflow: hidden; overflow: hidden;
height: 40px; height: 40px;
cursor: pointer; cursor: pointer;
@ -14,3 +27,12 @@
.custom-map-control-button:hover { .custom-map-control-button:hover {
background: rgb(235, 235, 235); background: rgb(235, 235, 235);
} }
.disabled-button {
opacity: 0.5;
cursor: not-allowed;
}
.disabled-button:hover {
background-color: #fff;
}

View File

@ -1,21 +1,26 @@
export default function distance(lat1, lon1, lat2, lon2, unit = 'K') { export default function distance(lat1, lon1, lat2, lon2, unit = "K") {
if ((lat1 == lat2) && (lon1 == lon2)) { if (lat1 == lat2 && lon1 == lon2) {
return 0; return 0
} } else {
else { var radlat1 = (Math.PI * lat1) / 180
var radlat1 = Math.PI * lat1 / 180; var radlat2 = (Math.PI * lat2) / 180
var radlat2 = Math.PI * lat2 / 180; var theta = lon1 - lon2
var theta = lon1 - lon2; var radtheta = (Math.PI * theta) / 180
var radtheta = Math.PI * theta / 180; var dist =
var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); Math.sin(radlat1) * Math.sin(radlat2) +
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta)
if (dist > 1) { if (dist > 1) {
dist = 1; dist = 1
} }
dist = Math.acos(dist); dist = Math.acos(dist)
dist = dist * 180 / Math.PI; dist = (dist * 180) / Math.PI
dist = dist * 60 * 1.1515; dist = dist * 60 * 1.1515
if (unit == "K") { dist = dist * 1.609344 } if (unit == "K") {
if (unit == "N") { dist = dist * 0.8684 } dist = dist * 1.609344
return dist; }
if (unit == "N") {
dist = dist * 0.8684
}
return dist
} }
} }

View File

@ -6,6 +6,9 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} },
"noUnusedLocals": true,
"experimentalDecorators": true,
"useDefineForClassFields": false,
} }
} }

3945
yarn.lock Normal file

File diff suppressed because it is too large Load Diff