Compare commits

...

47 Commits

Author SHA1 Message Date
榆木 f2f53eeb87
Merge b379834e5c into 8be95e32ba 2024-05-04 14:03:35 +02:00
Mike Cao 8be95e32ba
Merge pull request #2724 from umami-software/analytics
Team manager feature
2024-05-03 22:08:22 -07:00
Mike Cao ad72828000 Merge branch 'dev' into analytics 2024-05-03 22:06:48 -07:00
Mike Cao 84d7089c15
Merge pull request #2723 from umami-software/release
v2.11.3
2024-05-03 21:35:55 -07:00
Mike Cao 222e3d0705 Change version. 2024-05-03 21:01:29 -07:00
Francis Cao 9c06bc9893 add team manager role 2024-05-03 14:31:08 -07:00
Mike Cao b8cab762ca Merge branch 'analytics' of https://github.com/umami-software/umami into analytics 2024-05-03 10:00:35 -07:00
Mike Cao ca2a7f3354 Added query parameters to filters. 2024-05-03 00:13:28 -07:00
Mike Cao 8b5e690c3b Updated language bundles. 2024-05-02 23:58:58 -07:00
Mike Cao 63fe57321c Merge branch 'master' into dev 2024-05-02 23:49:57 -07:00
Mike Cao ceac4eb43f Merge branch 'dev' of https://github.com/umami-software/umami into dev 2024-05-02 23:48:58 -07:00
Mike Cao c4e3327dd8
Merge pull request #2676 from gerardnll/patch-1
Updated Catalan language
2024-05-02 23:48:53 -07:00
Mike Cao cf6a37f0f2
Merge pull request #2721 from mobeicanyue/patch-5
Update zh-CN.json
2024-05-02 23:47:00 -07:00
mobeicanyue abb7da8c95
Update zh-CN.json 2024-05-03 13:09:00 +08:00
Mike Cao 5f3998789d Merge branch 'dev' of https://github.com/umami-software/umami into dev 2024-05-02 19:58:31 -07:00
Mike Cao 85968d5c7c
Merge pull request #2687 from ccbikai/master
Update @netlify/plugin-nextjs to 5.x
2024-05-02 19:58:26 -07:00
Mike Cao 1b85c56e65 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2024-05-02 19:57:22 -07:00
Mike Cao 467b8a1b6b
Merge pull request #2700 from ruibinch/order-event-data-by-count-desc
Order event data in descending order by count
2024-05-02 19:57:15 -07:00
Mike Cao e1315c0c10 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2024-05-02 19:49:07 -07:00
Mike Cao 89f8b5e8ce
Merge pull request #2699 from Celil6p/master
Turkish language updated
2024-05-02 19:49:03 -07:00
Mike Cao e0510a815e Fixed types. 2024-05-02 19:48:56 -07:00
Mike Cao de9b6c9173
Merge pull request #2703 from sake92/add-bosnian-lang
Add Bosnian language
2024-05-02 19:46:43 -07:00
Mike Cao dd1795f08c
Merge pull request #2706 from mobeicanyue/patch-4
Update zh-CN.json
2024-05-02 19:45:30 -07:00
Mike Cao ab94f5d1cd Moved filter params into a hook. Close #2691. 2024-05-02 19:44:26 -07:00
Mike Cao 2ba87cd483 Removed usage limit reference. 2024-05-02 18:01:43 -07:00
Brian Cao 4eb01c5563 Merge branch 'dev' into analytics 2024-04-29 21:52:04 -07:00
Brian Cao 4e2bfd7485 Update admin/website search. Update param names. 2024-04-28 22:45:58 -07:00
Mike Cao 0fc8828f8b Refactored caching logic. 2024-04-26 00:31:38 -07:00
Mike Cao 216304a191 Removed blocking logic and duplicate validations. 2024-04-25 23:21:12 -07:00
Mike Cao 439f8a8aa3 Removed cache logic for websites. 2024-04-25 22:26:20 -07:00
Mike Cao 2620e8fe26 Merge remote-tracking branch 'origin/dev' into dev 2024-04-25 11:26:20 -07:00
Mike Cao 592c181902 Updated redis methods. 2024-04-25 11:26:10 -07:00
Francis Cao cfbc4ebd72 add filtering to event chart 2024-04-23 11:19:05 -07:00
Francis Cao fde2be4900 add minute to UNIT_TYPES. Fix yup validations when all-time filter returne start date = end date 2024-04-22 14:03:47 -07:00
Francis Cao 64d9a196cc renderteamURL on create report 2024-04-22 13:31:58 -07:00
Francis Cao 4b40b4e9cf stay in team context for website header links 2024-04-22 13:17:24 -07:00
Francis Cao 758b289a51 fix bug for teams UTM report 2024-04-22 12:58:58 -07:00
mobeicanyue 436f55c1a5
Update zh-CN.json 2024-04-23 02:16:22 +08:00
Sakib Hadziavdic 5aa9b20ff7 Add Bosnian language 2024-04-22 14:03:35 +02:00
Rui Bin Choo 22ab8b8222 order event data in descending order by count 2024-04-21 12:57:01 +08:00
Celil6p 81283ee9e4 Turkish language updated 2024-04-20 22:30:40 +03:00
榆木 b379834e5c
Bug Fix
Fix 'Argument `websiteId` is missing'
2024-04-20 19:05:34 +08:00
Mike Cao 4ca4be4445 Added types for session data. 2024-04-18 14:23:14 -07:00
Mike Cao 32cfb74c49 Updated check-env script. 2024-04-17 00:10:02 -07:00
Gerard Nesta f4310be501
Merge branch 'master' into patch-1 2024-04-17 08:48:22 +02:00
面条 c28de0fb1b chore: Update @netlify/plugin-nextjs to 5.x 2024-04-17 04:43:28 +00:00
Gerard Nesta f3283d693d
Update ca-ES.json 2024-04-15 11:00:45 +02:00
68 changed files with 3780 additions and 2192 deletions

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "2.11.2",
"version": "2.11.3",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT",
@ -71,7 +71,7 @@
"@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.28.6",
"@umami/prisma-client": "^0.14.0",
"@umami/redis-client": "^0.18.0",
"@umami/redis-client": "^0.20.0",
"chalk": "^4.1.1",
"chart.js": "^4.4.2",
"chartjs-adapter-date-fns": "^3.0.0",
@ -121,7 +121,7 @@
},
"devDependencies": {
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^4.41.3",
"@netlify/plugin-nextjs": "^5.1.0",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",

View File

@ -0,0 +1,251 @@
{
"AF": "Afganistan",
"AL": "Albanija",
"DZ": "Al\u017eir",
"VI": "Ameri\u010dka Djevi\u010danska ostrva",
"AS": "Ameri\u010dka Samoa",
"UM": "Ameri\u010dka Vanjska Ostrva",
"AD": "Andora",
"AO": "Angola",
"AI": "Angvila",
"AQ": "Antarktika",
"AG": "Antigva i Barbuda",
"AR": "Argentina",
"AM": "Armenija",
"AW": "Aruba",
"AU": "Australija",
"AT": "Austrija",
"AZ": "Azerbejd\u017ean",
"BS": "Bahami",
"BH": "Bahrein",
"BD": "Banglade\u0161",
"BB": "Barbados",
"BE": "Belgija",
"BZ": "Belize",
"BJ": "Benin",
"BM": "Bermuda",
"BY": "Bjelorusija",
"BW": "Bocvana",
"BO": "Bolivija",
"BA": "Bosna i Hercegovina",
"CX": "Bo\u017ei\u0107no ostrvo",
"BR": "Brazil",
"VG": "Britanska Djevi\u010danska ostrva",
"IO": "Britanska Teritorija u Indijskom Okeanu",
"BN": "Brunej",
"BG": "Bugarska",
"BF": "Burkina Faso",
"BI": "Burundi",
"BT": "Butan",
"CF": "Centralnoafri\u010dka Republika",
"ME": "Crna Gora",
"TD": "\u010cad",
"CZ": "\u010ce\u0161ka",
"CL": "\u010cile",
"DK": "Danska",
"CD": "Demokratska Republika Kongo",
"DM": "Dominika",
"DO": "Dominikanska Republika",
"DJ": "D\u017eibuti",
"EG": "Egipat",
"EC": "Ekvador",
"GQ": "Ekvatorijalna Gvineja",
"ER": "Eritreja",
"EE": "Estonija",
"SZ": "Esvatini",
"ET": "Etiopija",
"FO": "Farska ostrva",
"FJ": "Fid\u017ei",
"PH": "Filipini",
"FI": "Finska",
"FK": "Folklandska ostrva",
"FR": "Francuska",
"GF": "Francuska Gvajana",
"PF": "Francuska Polinezija",
"TF": "Francuske Ju\u017ene Teritorije",
"GA": "Gabon",
"GM": "Gambija",
"GH": "Gana",
"GG": "Gernzi",
"GI": "Gibraltar",
"GR": "Gr\u010dka",
"GD": "Grenada",
"GL": "Grenland",
"GE": "Gruzija",
"GU": "Guam",
"GP": "Gvadalupe",
"GY": "Gvajana",
"GT": "Gvatemala",
"GN": "Gvineja",
"GW": "Gvineja-Bisao",
"HT": "Haiti",
"HM": "Herd i arhipelag MekDonald",
"NL": "Holandija",
"HN": "Honduras",
"HK": "Hong Kong (SAR Kina)",
"HR": "Hrvatska",
"IN": "Indija",
"ID": "Indonezija",
"IQ": "Irak",
"IR": "Iran",
"IE": "Irska",
"IS": "Island",
"TL": "Isto\u010dni Timor",
"IT": "Italija",
"IL": "Izrael",
"JM": "Jamajka",
"JP": "Japan",
"YE": "Jemen",
"JE": "Jersey",
"JO": "Jordan",
"GS": "Ju\u017ena D\u017eord\u017eija i Ju\u017ena Sendvi\u010d ostrva",
"KR": "Ju\u017ena Koreja",
"SS": "Ju\u017eni Sudan",
"ZA": "Ju\u017enoafri\u010dka Republika",
"KY": "Kajmanska ostrva",
"KH": "Kambod\u017ea",
"CM": "Kamerun",
"CA": "Kanada",
"CV": "Kape Verde",
"BQ": "Karipska Holandija",
"QA": "Katar",
"KZ": "Kazahstan",
"KE": "Kenija",
"CN": "Kina",
"CY": "Kipar",
"KG": "Kirgistan",
"KI": "Kiribati",
"CC": "Kokosova (Keelingova) ostrva",
"CO": "Kolumbija",
"KM": "Komori",
"CG": "Kongo",
"CR": "Kostarika",
"CU": "Kuba",
"CK": "Kukova ostrva",
"CW": "Kurasao",
"KW": "Kuvajt",
"LA": "Laos",
"LV": "Latvija",
"LS": "Lesoto",
"LB": "Liban",
"LR": "Liberija",
"LY": "Libija",
"LI": "Lihten\u0161tajn",
"LT": "Litvanija",
"LU": "Luksemburg",
"MG": "Madagaskar",
"HU": "Ma\u0111arska",
"YT": "Majote",
"MO": "Makao (SAR Kina)",
"MW": "Malavi",
"MV": "Maldivi",
"MY": "Malezija",
"ML": "Mali",
"MT": "Malta",
"MA": "Maroko",
"MH": "Mar\u0161alova ostrva",
"MQ": "Martinik",
"MU": "Mauricijus",
"MR": "Mauritanija",
"MX": "Meksiko",
"FM": "Mikronezija",
"MM": "Mjanmar",
"MD": "Moldavija",
"MC": "Monako",
"MN": "Mongolija",
"MS": "Monserat",
"MZ": "Mozambik",
"NA": "Namibija",
"NR": "Nauru",
"NP": "Nepal",
"NE": "Niger",
"NG": "Nigerija",
"NI": "Nikaragva",
"NU": "Niue",
"NO": "Norve\u0161ka",
"NC": "Nova Kaledonija",
"NZ": "Novi Zeland",
"DE": "Njema\u010dka",
"CI": "Obala Slonova\u010de",
"AX": "Olandska ostrva",
"OM": "Oman",
"TC": "Ostrva Turks i Kaikos",
"WF": "Ostrva Valis i Futuna",
"BV": "Ostrvo Buve",
"IM": "Ostrvo Man",
"NF": "Ostrvo Norfolk",
"PK": "Pakistan",
"PW": "Palau",
"PS": "Palestinska Teritorija",
"PA": "Panama",
"PG": "Papua Nova Gvineja",
"PY": "Paragvaj",
"PE": "Peru",
"PN": "Pitkernska Ostrva",
"PL": "Poljska",
"PR": "Porto Riko",
"PT": "Portugal",
"RE": "Reunion",
"RW": "Ruanda",
"RO": "Rumunija",
"RU": "Rusija",
"SV": "Salvador",
"WS": "Samoa",
"SM": "San Marino",
"ST": "Sao Tome i Principe",
"SA": "Saudijska Arabija",
"SC": "Sej\u0161eli",
"SN": "Senegal",
"SL": "Sijera Leone",
"SG": "Singapur",
"SX": "Sint Marten",
"SY": "Sirija",
"US": "Sjedinjene Dr\u017eave",
"KP": "Sjeverna Koreja",
"MK": "Sjeverna Makedonija",
"MP": "Sjeverna Marijanska ostrva",
"SK": "Slova\u010dka",
"SI": "Slovenija",
"SB": "Solomonska Ostrva",
"SO": "Somalija",
"RS": "Srbija",
"SD": "Sudan",
"SR": "Surinam",
"SJ": "Svalbard i Jan Majen",
"SH": "Sveta Helena",
"LC": "Sveta Lucija",
"BL": "Sveti Bartolomej",
"KN": "Sveti Kits i Nevis",
"MF": "Sveti Martin",
"PM": "Sveti Petar i Mikelon",
"VC": "Sveti Vinsent i Grenadin",
"ES": "\u0160panija",
"LK": "\u0160ri Lanka",
"SE": "\u0160vedska",
"CH": "\u0160vicarska",
"TJ": "Tad\u017eikistan",
"TH": "Tajland",
"TW": "Tajvan",
"TZ": "Tanzanija",
"TG": "Togo",
"TK": "Tokelau",
"TO": "Tonga",
"TT": "Trinidad i Tobago",
"TN": "Tunis",
"TM": "Turkmenistan",
"TR": "Turska",
"TV": "Tuvalu",
"UG": "Uganda",
"AE": "Ujedinjeni Arapski Emirati",
"GB": "Ujedinjeno Kraljevstvo",
"UA": "Ukrajina",
"UY": "Urugvaj",
"UZ": "Uzbekistan",
"VU": "Vanuatu",
"VA": "Vatikan",
"VE": "Venecuela",
"VN": "Vijetnam",
"ZM": "Zambija",
"EH": "Zapadna Sahara",
"ZW": "Zimbabve"
}

View File

@ -0,0 +1,611 @@
{
"ab": "abhazijski",
"ace": "a\u010dineski",
"ada": "adangmejski",
"ady": "adigejski",
"aa": "afarski",
"afh": "afrihili",
"af": "afrikanerski",
"agq": "aghem",
"ain": "ainu",
"ay": "ajmara",
"akk": "akadijski",
"ak": "akan",
"ach": "akoli",
"bss": "Akoose",
"akz": "Alabama",
"sq": "albanski",
"arq": "Algerian Arabic",
"ale": "aljut",
"ase": "American Sign Language",
"en_US": "ameri\u010dki engleski",
"am": "amharski",
"anp": "angika",
"njo": "Ao Naga",
"an": "aragone\u017eanski",
"aro": "Araona",
"arp": "arapaho",
"ar": "arapski",
"arn": "araukanski",
"arw": "aravak",
"arc": "armajski",
"rup": "aromanijski",
"frp": "Arpitan",
"as": "asemijski",
"ast": "asturijski",
"asa": "asu",
"cch": "atsam",
"en_AU": "australski engleski",
"de_AT": "austrijski njema\u010dki",
"awa": "avadhi",
"av": "avarski",
"ae": "avestanski",
"az": "azerbejd\u017eanski",
"bfq": "Badaga",
"ksf": "bafia",
"bfd": "Bafut",
"bqi": "Bakhtiari",
"ban": "balinezijski",
"bal": "balu\u010di",
"bm": "bambara",
"bax": "Bamun",
"bjn": "Banjar",
"bas": "basa",
"eu": "baskijski",
"ba": "ba\u0161kirski",
"bbc": "Batak Toba",
"bar": "Bavarian",
"bej": "beja",
"bem": "bemba",
"bez": "bena",
"bn": "bengalski",
"bew": "Betawi",
"zxx": "bez lingvisti\u010dkog sadr\u017eaja",
"bik": "bikol",
"bin": "bini",
"bpy": "Bishnupriya",
"bi": "bislama",
"be": "bjeloruski",
"byn": "blin",
"zbl": "blisimboli",
"brx": "bodo",
"bho": "bojpuri",
"bs": "bosanski",
"brh": "Brahui",
"bra": "braj",
"pt_BR": "Brazilian Portuguese",
"br": "bretonski",
"en_GB": "britanski engleski",
"bg": "bugarski",
"bug": "bugine\u017eanskii",
"bum": "Bulu",
"bua": "buriat",
"my": "burmanski",
"frc": "Cajun French",
"yue": "Cantonese",
"cps": "Capiznon",
"cay": "Cayuga",
"ceb": "cebuano",
"dtp": "Central Dusun",
"esu": "Central Yupik",
"shu": "Chadian Arabic",
"qug": "Chimborazo Highland Quichua",
"ksh": "Colognian",
"swb": "Comorian",
"cy": "cy",
"chg": "\u010dagatai",
"ch": "\u010damoro",
"ce": "\u010de\u010denski",
"chy": "\u010dejenski",
"cs": "\u010de\u0161ki",
"chb": "\u010dib\u010da",
"cgg": "\u010diga",
"chn": "\u010dinukski",
"chp": "\u010dipvijanski",
"chr": "\u010diroki",
"cho": "\u010doktavski",
"chk": "\u010dukeski",
"cv": "\u010duva\u0161ki",
"dak": "dakota",
"da": "danski",
"dar": "dargva",
"dzg": "Dazaga",
"del": "delaver",
"din": "dinka",
"dv": "divehijski",
"doi": "dogri",
"dgr": "dogrib",
"dsb": "donjolu\u017ei\u010dkosrpski",
"dua": "duala",
"gez": "d\u017eiz",
"dz": "d\u017eonga",
"dyu": "\u0111ula",
"efi": "efikski",
"arz": "Egyptian Arabic",
"eka": "ekajuk",
"elx": "elamitski",
"ebu": "embu",
"egl": "Emilian",
"en": "engleski",
"myv": "erzija",
"eo": "esperanto",
"et": "estonski",
"pt_PT": "European Portuguese",
"ee": "eve",
"ewo": "evondo",
"es_ES": "evropski \u0161panski",
"ext": "Extremaduran",
"fan": "fang",
"fat": "fanti",
"fo": "farski",
"phn": "feni\u010danski",
"fj": "fid\u017eijski",
"hif": "Fiji Hindi",
"fil": "filipinski",
"fi": "finski",
"nl_BE": "flamanski",
"fon": "fon",
"gur": "Frafra",
"fr": "francuski",
"fur": "friulijski",
"fy": "frizijski",
"ff": "fulah",
"gaa": "ga",
"gag": "gagau\u0161ki",
"gay": "gajo",
"gl": "galski",
"gan": "Gan Chinese",
"lg": "ganda",
"gba": "gbaja",
"aln": "Gheg Albanian",
"bbj": "Ghomala",
"glk": "Gilaki",
"gil": "gilbert\u0161ki",
"gom": "Goan Konkani",
"gon": "gondi",
"hsb": "gornjolu\u017ei\u010dkosrpski",
"de_CH": "gornjonjema\u010dki (\u0161vicarski)",
"gor": "gorontalo",
"got": "gotski",
"el": "gr\u010dki",
"grb": "grebo",
"ka": "gruzijski",
"gu": "gud\u017earati",
"guz": "gusii",
"gn": "gvarani",
"gwi": "Gwich\u02bcin",
"ht": "hai\u0107anski",
"hai": "haida",
"hak": "Hakka Chinese",
"ha": "hausa",
"haw": "havajski",
"he": "hebrejski",
"hz": "herero",
"hil": "hiligajnon",
"hi": "hindi",
"ho": "hiri motu",
"hit": "hitite",
"hmn": "hmong",
"nl": "holandski",
"hr": "hrvatski",
"hup": "hupa",
"iba": "iban",
"ibb": "Ibibio",
"io": "ido",
"ig": "igbo",
"ilo": "iloko",
"smn": "inari sami",
"id": "indonezijski",
"izh": "Ingrian",
"inh": "ingu\u0161etski",
"ia": "interlingva",
"ie": "interlingve",
"iu": "inuktitut",
"ik": "inupiak",
"ga": "irski",
"is": "islandski",
"frs": "isto\u010dni frizijski",
"it": "italijanski",
"sah": "jakut",
"jam": "Jamaican Creole English",
"yao": "jao",
"ja": "japanski",
"yap": "jape\u0161ki",
"jv": "javanski",
"hy": "jermenski",
"yi": "jidi\u0161",
"dyo": "jola-fonyi",
"yo": "jorubanski",
"jrb": "judeo-arapski",
"jpr": "judeo-persijski",
"jut": "Jutish",
"alt": "ju\u017eni altai",
"nr": "ju\u017eni ndebele",
"sma": "ju\u017eni sami",
"kbd": "kabardijski",
"kab": "kabile",
"kac": "ka\u010din",
"cad": "kado",
"kgp": "Kaingang",
"kkj": "Kako",
"kl": "kalalisutski",
"kln": "kalenjin",
"xal": "kalmik",
"kam": "kamba",
"kn": "kanada",
"en_CA": "kanadski engleski",
"fr_CA": "kanadski francuski",
"kbl": "Kanembu",
"kr": "kanuri",
"kaa": "kara-kalpa\u0161ki",
"krc": "kara\u010daj-balkar",
"krl": "karelijski",
"car": "karipski",
"kha": "kasi",
"ks": "ka\u0161miri",
"csb": "ka\u0161ubijanski",
"ca": "katalonski",
"kaw": "kavi",
"kk": "kaza\u010dki",
"ken": "Kenyang",
"khw": "Khowar",
"quc": "ki\u010de",
"ki": "kikuju",
"kmb": "kimbundu",
"krj": "Kinaray-a",
"zh": "kineski",
"zh_Hans": "kineski (pojednostavljeni)",
"zh_Hant": "kineski (tradicionalni)",
"rw": "kinjarvanda",
"ky": "kirgiski",
"kiu": "Kirmanjki",
"nwc": "klasi\u010dni nevari",
"syc": "klasi\u010dni sirijski",
"tlh": "klingonski",
"km": "kmerski",
"ses": "kojraboro seni",
"bkm": "Kom",
"kv": "komi",
"koi": "komi-permja\u010dki",
"kg": "kongo",
"swc": "kongoanski swahili",
"kok": "konkani",
"cop": "koptski",
"ko": "korejski",
"kw": "korni\u0161ki",
"kfo": "koro",
"co": "korzikanski",
"xh": "kosa",
"kos": "kosreanski",
"kho": "kotanizijski",
"avk": "Kotava",
"khq": "koyra chiini",
"kpe": "kpele",
"cr": "kri",
"crh": "krimeanski turski",
"kri": "Krio",
"mus": "kri\u0161ki",
"kj": "kuanjama",
"kum": "kumik",
"ku": "kurdski",
"kru": "kurukh",
"kut": "kutenai",
"qu": "kven\u010da",
"nmg": "kwasio",
"lad": "ladino",
"lkt": "lakota",
"lam": "lamba",
"lah": "landa",
"lag": "langi",
"lo": "lao\u0161ki",
"ltg": "Latgalian",
"es_419": "latinoameri\u010dki \u0161panski",
"la": "latinski",
"lzz": "Laz",
"lv": "letonski",
"lez": "lezgian",
"lij": "Ligurian",
"li": "limburgi\u0161",
"ln": "lingala",
"lfn": "Lingua Franca Nova",
"lzh": "Literary Chinese",
"lt": "litvanski",
"liv": "Livonian",
"jbo": "lojban",
"lmo": "Lombard",
"sli": "Lower Silesian",
"loz": "lozi",
"lu": "luba-katanga",
"lua": "luba-lulua",
"lui": "luiseno",
"lb": "luksembur\u0161ki",
"smj": "lule sami",
"lun": "lunda",
"luo": "luo",
"lus": "lu\u0161ai",
"luy": "luyia",
"mde": "Maba",
"jmc": "machame",
"mad": "madure\u0161ki",
"hu": "ma\u0111arski",
"maf": "Mafa",
"mag": "magahi",
"moh": "mahavski",
"vmf": "Main-Franconian",
"mai": "maitili",
"mak": "makasar",
"mk": "makedonski",
"mgh": "makhuwa-meetto",
"kde": "makonde",
"mg": "malagazijski",
"ml": "malajalam",
"ms": "malajski",
"mt": "malte\u0161ki",
"mnc": "man\u010du",
"mdr": "mandar",
"man": "mandingo",
"mni": "manipuri",
"gv": "manks",
"mi": "maorski",
"mr": "marati",
"chm": "mari",
"tzm": "marokanski tamazigt",
"mh": "mar\u0161alski",
"mwr": "marvari",
"mas": "masai",
"mfe": "mauricijski kreolski",
"mzn": "Mazanderani",
"byv": "Medumba",
"es_MX": "meksi\u010dki \u0161panski",
"men": "mende",
"mwv": "Mentawai",
"mer": "meru",
"mgo": "meta\u2019",
"mic": "mikmak",
"nan": "Min Nan Chinese",
"min": "minangkabau",
"xmf": "Mingrelian",
"mwl": "mirande\u0161ki",
"ar_001": "moderni standardni arapski",
"mdf": "mok\u0161a",
"ro_MD": "moldavski",
"lol": "mongo",
"mn": "mongolski",
"ary": "Moroccan Arabic",
"mos": "mosi",
"mua": "mundang",
"ttt": "Muslim Tat",
"mye": "Myene",
"naq": "nama",
"na": "nauru",
"nv": "navaho",
"ng": "ndonga",
"nap": "neapolitanski",
"ne": "nepalski",
"und": "nepoznati ili neva\u017ee\u0107i jezik",
"new": "nevari",
"sba": "Ngambay",
"nnh": "Ngiemboon",
"jgo": "ngomba",
"yrl": "Nheengatu",
"nia": "nias",
"nds": "niski nema\u010dki",
"niu": "niuean",
"nqo": "nko",
"nog": "nogai",
"no": "norve\u0161ki",
"nb": "norve\u0161ki bokmal",
"nn": "norve\u0161ki njorsk",
"nov": "Novial",
"nus": "nuer",
"nzi": "nzima",
"nym": "njamvezi",
"nyn": "njankole",
"ny": "njanja",
"tog": "njasa tonga",
"de": "njema\u010dki",
"nyo": "njoro",
"oj": "ojibva",
"or": "orijski",
"om": "oromo",
"osa": "osage",
"os": "osetski",
"ota": "otomanski turski",
"pal": "pahlavi",
"pfl": "Palatine German",
"pau": "palauanski",
"pi": "pali",
"pam": "pampanga",
"pa": "pand\u017eabski",
"pag": "pangasinski",
"pap": "papiamento",
"ps": "pa\u0161tunski",
"pdc": "Pennsylvania German",
"fa": "perzijski",
"pcd": "Picard",
"pms": "Piedmontese",
"pdt": "Plautdietsch",
"pl": "poljski",
"pon": "ponpejski",
"pnt": "Pontic",
"pt": "portugalski",
"oc": "provansalski",
"prg": "Prussian",
"raj": "ra\u0111astani",
"rap": "rapanui",
"rar": "rarotongan",
"rm": "reto-romanski",
"rif": "Riffian",
"rgn": "Romagnol",
"rom": "romani",
"rof": "rombo",
"rtm": "Rotuman",
"rug": "Roviana",
"rwk": "rua",
"ro": "rumunski",
"root": "run",
"rn": "rundi",
"ru": "ruski",
"rue": "Rusyn",
"ssy": "Saho",
"sam": "samaritanski aramejski",
"saq": "samburu",
"sm": "samoanski",
"sgs": "Samogitian",
"sad": "sandave",
"sg": "sango",
"sbp": "sangu",
"sa": "sanskrit",
"sat": "santali",
"sc": "sardinijski",
"sas": "sasak",
"sdc": "Sassarese Sardinian",
"stq": "Saterland Frisian",
"saz": "Saurashtra",
"sly": "Selayar",
"sel": "selkap",
"seh": "sena",
"see": "Seneca",
"srr": "serer",
"sei": "Seri",
"st": "sesoto",
"nso": "severni soto",
"frr": "severno-frizijski",
"ksb": "shambala",
"scn": "sicilijanski",
"ii": "si\u010duan ji",
"sid": "sidamo",
"bla": "siksika",
"szl": "Silesian",
"sd": "sindi",
"si": "singaleski",
"syr": "sirijski",
"nd": "sjeverni ndebele",
"se": "sjeverni sami",
"sms": "skoltski jezik",
"den": "slavski",
"sk": "slova\u010dki",
"sl": "slovena\u010dki",
"sog": "sod\u017eijenski",
"xog": "soga",
"so": "somalski",
"snk": "soninke",
"ckb": "soranski kurdski",
"azb": "South Azerbaijani",
"srn": "srananski tongo",
"enm": "srednji engleski",
"frm": "srednji francuski",
"dum": "srednji holandski",
"mga": "srednji irski",
"gmh": "srednji visoki nema\u010dki",
"sr": "srpski",
"sh": "srpskohrvatski",
"zgh": "standardni marokanski tamazigt",
"non": "stari norski",
"egy": "staroegipatski",
"ang": "staroengleski",
"fro": "starofrancuski",
"grc": "starogr\u010dki",
"sga": "staroirski",
"goh": "staronema\u010dki",
"peo": "staropersijski",
"pro": "staroprovansalski",
"cu": "staroslovenski",
"su": "sudanski",
"suk": "sukuma",
"sux": "sumerski",
"sus": "susu",
"sw": "svahili",
"ss": "svati",
"shn": "\u0161an",
"sco": "\u0161kotski",
"gd": "\u0161kotski galski",
"sn": "\u0161ona",
"es": "\u0161panski",
"fr_CH": "\u0161vajcarski francuski",
"gsw": "\u0161vajcarski njema\u010dki",
"sv": "\u0161vedski",
"tg": "tad\u017ei\u010dki",
"tl": "tagalski",
"shi": "tahelhit",
"ty": "tahi\u0107anski",
"dav": "taita",
"th": "tajlandski",
"tly": "Talysh",
"tmh": "tama\u0161ek",
"ta": "tamilski",
"trv": "Taroko",
"twq": "tasavak",
"tt": "tatarski",
"te": "telugu",
"ter": "tereno",
"teo": "teso",
"tet": "tetum",
"bo": "tibetanski",
"tig": "tigre",
"ti": "tigrinja",
"tem": "timne",
"tiv": "tiv",
"kcg": "tjap",
"tli": "tlingit",
"tpi": "tok pisin",
"tkl": "tokelau",
"to": "tonga",
"fit": "Tornedalen Finnish",
"tkr": "Tsakhur",
"tsd": "Tsakonian",
"tsi": "tsim\u0161ian",
"ts": "tsonga",
"tn": "tsvana",
"tcy": "Tulu",
"tum": "tumbuka",
"aeb": "Tunisian Arabic",
"tk": "turkmenski",
"tru": "Turoyo",
"tr": "turski",
"tvl": "tuvalu",
"tyv": "tuvinijski",
"tw": "tvi",
"udm": "udmurt",
"uga": "ugaritski",
"ug": "ujgurski",
"uk": "ukrajinski",
"umb": "umbundu",
"ur": "urdu",
"uz": "uzbe\u010dki",
"vai": "vai",
"wal": "valamo",
"wa": "valun",
"war": "varej",
"was": "va\u0161o",
"ve": "venda",
"vec": "Venetian",
"vep": "Veps",
"vi": "vijetnamski",
"mul": "vi\u0161e jezika",
"vo": "volap\u00fck",
"wo": "volof",
"vro": "V\u00f5ro",
"vot": "votski",
"vun": "vunjo",
"wae": "Walser",
"wbp": "Warlpiri",
"guc": "Wayuu",
"vls": "West Flemish",
"mrj": "Western Mari",
"wuu": "Wu Chinese",
"hsn": "Xiang Chinese",
"yav": "Yangben",
"ybb": "Yemba",
"zap": "zapote\u010dki",
"dje": "zarma",
"zza": "zaza",
"zea": "Zeelandic",
"kea": "zelenortski",
"zen": "zenaga",
"gbz": "Zoroastrian Dari",
"za": "zuang",
"zu": "zulu",
"zun": "zuni",
"kaj": "\u017eju"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
"label.add-step": [
{
"type": 0,
"value": "Add step"
"value": "添加步骤"
}
],
"label.add-website": [
@ -564,7 +564,7 @@
"label.last-months": [
{
"type": 0,
"value": "Last "
"value": "最近 "
},
{
"type": 1,
@ -572,7 +572,7 @@
},
{
"type": 0,
"value": " months"
"value": " 个月"
}
],
"label.leave": [
@ -996,7 +996,7 @@
"label.steps": [
{
"type": 0,
"value": "Steps"
"value": "步骤"
}
],
"label.sum": [
@ -1176,7 +1176,7 @@
"label.update": [
{
"type": 0,
"value": "Update"
"value": "更新"
}
],
"label.url": [
@ -1218,7 +1218,7 @@
"label.utm-description": [
{
"type": 0,
"value": "Track your campaigns through UTM parameters."
"value": "通过UTM参数追踪您的广告活动。"
}
],
"label.value": [
@ -1254,7 +1254,7 @@
"label.views-per-visit": [
{
"type": 0,
"value": "Views per visit"
"value": "每次访问的浏览量"
}
],
"label.visitors": [
@ -1266,7 +1266,7 @@
"label.visits": [
{
"type": 0,
"value": "Visits"
"value": "访问次数"
}
],
"label.website": [

View File

@ -23,13 +23,9 @@ if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) {
}
if (process.env.CLICKHOUSE_URL) {
checkMissing(['CA_CERT', 'CLIENT_CERT', 'CLIENT_KEY', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']);
checkMissing(['KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']);
}
if (process.env.CLOUD_MODE) {
checkMissing(['CLOUD_URL']);
}
if (process.env.ENABLE_BLOCKER) {
checkMissing(['REDIS_URL']);
}

View File

@ -40,6 +40,9 @@ export function TeamMemberEditForm({
};
const renderValue = (value: string) => {
if (value === ROLES.teamManager) {
return formatMessage(labels.manager);
}
if (value === ROLES.teamMember) {
return formatMessage(labels.member);
}
@ -58,6 +61,7 @@ export function TeamMemberEditForm({
minWidth: '250px',
}}
>
<Item key={ROLES.teamManager}>{formatMessage(labels.manager)}</Item>
<Item key={ROLES.teamMember}>{formatMessage(labels.member)}</Item>
<Item key={ROLES.teamViewOnly}>{formatMessage(labels.viewOnly)}</Item>
</Dropdown>

View File

@ -12,8 +12,10 @@ export function TeamMembersPage({ teamId }: { teamId: string }) {
const { formatMessage, labels } = useMessages();
const canEdit =
team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
user.role !== ROLES.viewOnly;
team?.teamUser?.find(
({ userId, role }) =>
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
) && user.role !== ROLES.viewOnly;
return (
<>

View File

@ -19,6 +19,7 @@ export function TeamMembersTable({
const roles = {
[ROLES.teamOwner]: formatMessage(labels.teamOwner),
[ROLES.teamManager]: formatMessage(labels.teamManager),
[ROLES.teamMember]: formatMessage(labels.teamMember),
[ROLES.teamViewOnly]: formatMessage(labels.viewOnly),
};

View File

@ -15,18 +15,24 @@ export function TeamDetails({ teamId }: { teamId: string }) {
const { user } = useLogin();
const [tab, setTab] = useState('details');
const canEdit =
const isTeamOwner =
!!team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
user.role !== ROLES.viewOnly;
const canEdit =
!!team?.teamUser?.find(
({ userId, role }) =>
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
) && user.role !== ROLES.viewOnly;
return (
<Flexbox direction="column">
<PageHeader title={team?.name} icon={<Icons.Users />}>
{!canEdit && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</PageHeader>
<Tabs selectedKey={tab} onSelect={(value: any) => setTab(value)} style={{ marginBottom: 30 }}>
<Item key="details">{formatMessage(labels.details)}</Item>
{canEdit && <Item key="manage">{formatMessage(labels.manage)}</Item>}
{isTeamOwner && <Item key="manage">{formatMessage(labels.manage)}</Item>}
</Tabs>
{tab === 'details' && <TeamEditForm teamId={teamId} allowEdit={canEdit} />}
{tab === 'manage' && <TeamManage teamId={teamId} />}

View File

@ -0,0 +1,3 @@
import Page from 'app/(main)/reports/utm/page';
export default Page;

View File

@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/event-data/page';
export default Page;

View File

@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/realtime/page';
export default Page;

View File

@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/reports/page';
export default Page;

View File

@ -1,6 +1,6 @@
import classNames from 'classnames';
import Favicon from 'components/common/Favicon';
import { useMessages, useWebsite } from 'components/hooks';
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
import Icons from 'components/icons';
import ActiveUsers from 'components/metrics/ActiveUsers';
import Link from 'next/link';
@ -19,6 +19,7 @@ export function WebsiteHeader({
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
const pathname = usePathname();
const { data: website } = useWebsite(websiteId);
const { name, domain } = website || {};
@ -62,7 +63,11 @@ export function WebsiteHeader({
: pathname.match(/^\/websites\/[\w-]+$/);
return (
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
<Link
key={label}
href={renderTeamUrl(`/websites/${websiteId}${path}`)}
shallow={true}
>
<Button
variant="quiet"
className={classNames({

View File

@ -1,18 +1,19 @@
'use client';
import Link from 'next/link';
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages, useTeamUrl } from 'components/hooks';
import WebsiteHeader from '../WebsiteHeader';
import ReportsDataTable from 'app/(main)/reports/ReportsDataTable';
export function WebsiteReportsPage({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
return (
<>
<WebsiteHeader websiteId={websiteId} />
<Flexbox alignItems="center" justifyContent="end">
<Link href={`/reports/create`}>
<Link href={renderTeamUrl('/reports/create')}>
<Button variant="primary">
<Icon>
<Icons.Plus />

View File

@ -1,14 +1,14 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react';
import { useApi } from './useApi';
import { FilterResult, SearchFilter, FilterQueryResult } from 'lib/types';
import { PageResult, PageParams, FilterQueryResult } from 'lib/types';
export function useFilterQuery<T = any>({
queryKey,
queryFn,
...options
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> {
const [params, setParams] = useState<T | SearchFilter>({
const [params, setParams] = useState<T | PageParams>({
query: '',
page: 1,
});
@ -21,7 +21,7 @@ export function useFilterQuery<T = any>({
});
return {
result: data as FilterResult<any>,
result: data as PageResult<any>,
query,
params,
setParams,

View File

@ -1,33 +1,17 @@
import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
import { UseQueryOptions } from '@tanstack/react-query';
import { useDateRange, useNavigation, useTimezone } from 'components/hooks';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useWebsiteEvents(
websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, offset } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, event },
} = useNavigation();
const params = {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
offset,
timezone,
url,
event,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['events', { ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, { ...params }),
queryKey: ['websites:events', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, params),
enabled: !!websiteId,
...options,
});

View File

@ -1,12 +1,15 @@
import useApi from './useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteMetrics(
websiteId: string,
params?: { [key: string]: any },
type: string,
limit: number,
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
return useQuery({
queryKey: [
@ -14,21 +17,26 @@ export function useWebsiteMetrics(
{
websiteId,
...params,
type,
limit,
},
],
queryFn: async () => {
const filters = { ...params };
filters[params.type] = undefined;
filters[type] = undefined;
const data = await get(`/websites/${websiteId}/metrics`, {
...filters,
type,
limit,
});
options?.onDataLoad?.(data);
return data;
},
enabled: !!websiteId,
...options,
});
}

View File

@ -1,34 +1,18 @@
import { zonedTimeToUtc } from 'date-fns-tz';
import { useApi, useDateRange, useNavigation, useTimezone } from 'components/hooks';
import { UseQueryOptions } from '@tanstack/react-query';
import { useApi } from './useApi';
import { useFilterParams } from '..//useFilterParams';
export function useWebsitePageviews(websiteId: string, options?: { [key: string]: string }) {
export function useWebsitePageviews(
websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
} = useNavigation();
const params = {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
timezone,
url,
referrer,
os,
browser,
device,
country,
region,
city,
title,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:pageviews', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
enabled: !!websiteId,
...options,
});
}

View File

@ -1,30 +1,14 @@
import { useApi, useDateRange, useNavigation } from 'components/hooks';
import { useApi } from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const {
query: { url, referrer, title, os, browser, device, country, region, city },
} = useNavigation();
const params = {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:stats', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/stats`, params),
enabled: !!websiteId,
...options,
});
}

View File

@ -0,0 +1,32 @@
import { useNavigation } from './useNavigation';
import { useDateRange } from './useDateRange';
import { useTimezone } from './useTimezone';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useFilterParams(websiteId: string) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, offset } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, referrer, title, query, os, browser, device, country, region, city, event },
} = useNavigation();
return {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
offset,
timezone,
url,
referrer,
title,
query,
os,
browser,
device,
country,
region,
city,
event,
};
}

View File

@ -29,6 +29,7 @@ export const labels = defineMessages({
createdBy: { id: 'label.created-by', defaultMessage: 'Created By' },
edit: { id: 'label.edit', defaultMessage: 'Edit' },
name: { id: 'label.name', defaultMessage: 'Name' },
manager: { id: 'label.manager', defaultMessage: 'Manager' },
member: { id: 'label.member', defaultMessage: 'Member' },
members: { id: 'label.members', defaultMessage: 'Members' },
accessCode: { id: 'label.access-code', defaultMessage: 'Access code' },
@ -43,6 +44,7 @@ export const labels = defineMessages({
settings: { id: 'label.settings', defaultMessage: 'Settings' },
owner: { id: 'label.owner', defaultMessage: 'Owner' },
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
teamManager: { id: 'label.team-manager', defaultMessage: 'Team manager' },
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
teamViewOnly: { id: 'label.team-view-only', defaultMessage: 'Team view only' },
enableShareUrl: { id: 'label.enable-share-url', defaultMessage: 'Enable share URL' },

View File

@ -6,7 +6,6 @@ import LinkButton from 'components/common/LinkButton';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { percentFilter } from 'lib/filters';
import {
useDateRange,
useNavigation,
useWebsiteMetrics,
useMessages,
@ -45,34 +44,14 @@ export function MetricsTable({
}: MetricsTableProps) {
const [search, setSearch] = useState('');
const { formatValue } = useFormat();
const [{ startDate, endDate }] = useDateRange(websiteId);
const {
renderUrl,
query: { url, referrer, title, os, browser, device, country, region, city },
} = useNavigation();
const { renderUrl } = useNavigation();
const { formatMessage, labels } = useMessages();
const { dir } = useLocale();
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
websiteId,
{
type,
startAt: +startDate,
endAt: +endDate,
url,
referrer,
os,
title,
browser,
device,
country,
region,
city,
limit,
search,
},
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
);
const { data, isLoading, isFetched, error } = useWebsiteMetrics(websiteId, type, limit, {
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
onDataLoad,
});
const filteredData = useMemo(() => {
if (data) {

View File

@ -38,7 +38,11 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
id={view}
value={x}
label={!x && formatMessage(labels.none)}
externalUrl={`${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`}
externalUrl={
view === 'url'
? `${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`
: null
}
/>
);
};

246
src/lang/bs-BA.json Normal file
View File

@ -0,0 +1,246 @@
{
"label.access-code": "Pristupni kod",
"label.actions": "Akcije",
"label.activity-log": "Log aktivnosti",
"label.add": "Dodaj",
"label.add-description": "Dodaj opis",
"label.add-member": "Dodaj člana",
"label.add-step": "Dodaj korak",
"label.add-website": "Dodaj web stranicu",
"label.admin": "Administrator",
"label.after": "Nakon",
"label.all": "Sve",
"label.all-time": "Cijelo vrijeme",
"label.analytics": "Analitike",
"label.average": "Prosjek",
"label.average-visit-time": "Prosječno vrijeme posjete",
"label.back": "Nazad",
"label.before": "Prije",
"label.bounce-rate": "Bounce rate",
"label.breakdown": "Pregled po kategorijama",
"label.browser": "Browser",
"label.browsers": "Browseri",
"label.cancel": "Otkaži",
"label.change-password": "Promijeni šifru",
"label.cities": "Gradovi",
"label.city": "Grad",
"label.clear-all": "Očisti sve",
"label.confirm": "Potvrdi",
"label.confirm-password": "Potvrdi šifru",
"label.contains": "Sadrži",
"label.continue": "Nastavi",
"label.countries": "Zemlje",
"label.country": "Zemlja",
"label.create": "Kreiraj",
"label.create-report": "Kreiraj izvještaj",
"label.create-team": "Kreiraj tim",
"label.create-user": "Kreiraj korisnika",
"label.created": "Kreiraj",
"label.created-by": "Kreirao",
"label.current-password": "Trenutna šifra",
"label.custom-range": "Proizvoljni raspon",
"label.dashboard": "Dashboard",
"label.data": "Podaci",
"label.date": "Datum",
"label.date-range": "Datumski raspon",
"label.day": "Dan",
"label.default-date-range": "Defaultni datumski raspon",
"label.delete": "Izbriši",
"label.delete-report": "Izbriši report",
"label.delete-team": "Izbriši tim",
"label.delete-user": "Izbriši korisnika",
"label.delete-website": "Izbriši web stranicu",
"label.description": "Opis",
"label.desktop": "Desktop",
"label.details": "Detalji",
"label.device": "Uređaj",
"label.devices": "Uređaji",
"label.dismiss": "Odbaci",
"label.does-not-contain": "Ne sadrži",
"label.domain": "Domena",
"label.dropoff": "Dropoff",
"label.edit": "Uredi",
"label.edit-dashboard": "Uredi dashboard",
"label.edit-member": "Uredi člana",
"label.enable-share-url": "Omogući URL za dijeljenje",
"label.event": "Događaj",
"label.event-data": "Podaci o događaju",
"label.events": "Događaji",
"label.false": "Ne",
"label.field": "Polje",
"label.fields": "Polja",
"label.filter": "Filter",
"label.filter-combined": "Kombinovano",
"label.filter-raw": "Sirovo",
"label.filters": "Filtri",
"label.funnel": "Lijevak",
"label.funnel-description": "Razumite koverziju i drop-off učestalost korisnika.",
"label.greater-than": "Veće od",
"label.greater-than-equals": "Veće od ili jednako",
"label.insights": "Uvidi",
"label.insights-description": "Zaronite dublje u vaše podatke korištenjem segmenata i filtera",
"label.is": "Jeste",
"label.is-not": "Nije",
"label.is-not-set": "Nije setano",
"label.is-set": "Jeste setano",
"label.join": "Učlani se",
"label.join-team": "Učlani se u tim",
"label.language": "Jezik",
"label.languages": "Jezici",
"label.laptop": "Laptop",
"label.last-days": "Zadnjih {x} dana",
"label.last-hours": "Zadnjih {x} sati",
"label.last-months": "Zadnjih {x} mjeseci",
"label.leave": "Napusti",
"label.leave-team": "Napusti tim",
"label.less-than": "Manje od",
"label.less-than-equals": "Manje od ili jednako",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "Manage",
"label.max": "Max",
"label.member": "Član",
"label.members": "Članovi",
"label.min": "Min",
"label.mobile": "Mobile",
"label.more": "Više",
"label.my-account": "Moj račun",
"label.my-websites": "Moje web stranice",
"label.name": "Ime",
"label.new-password": "Nova šifra",
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.os": "OS",
"label.overview": "Pregled",
"label.owner": "Vlasnik",
"label.page-of": "Strana {current} od {total}",
"label.page-views": "Pregleda stranica",
"label.pageTitle": "Naslov stranice",
"label.pages": "Stranice",
"label.password": "Šifra",
"label.powered-by": "Omogućeno s {name}",
"label.profile": "Profil",
"label.queries": "Queryji",
"label.query": "Query",
"label.query-parameters": "Query parametri",
"label.realtime": "Realno vrijeme",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Refresh",
"label.regenerate": "Regeneriši",
"label.region": "Region",
"label.regions": "Regioni",
"label.remove": "Ukloni",
"label.remove-member": "Ukloni člana",
"label.reports": "Izvještaji",
"label.required": "Required",
"label.reset": "Resetuj",
"label.reset-website": "Resetuj web stranicu",
"label.retention": "Retention",
"label.retention-description": "Izmjeri 'ljepljivost' svoje web stranice praćenjem koliko često set korisnici vraćaju.",
"label.role": "Rola",
"label.run-query": "Pokreni query",
"label.save": "Sačuvaj",
"label.screens": "Ekrani",
"label.search": "Traži",
"label.select": "Odaberi",
"label.select-date": "Odaberi datum",
"label.select-role": "Odaberi rolu",
"label.select-website": "Odaberi web stranicu",
"label.sessions": "Sesije",
"label.settings": "Postavke",
"label.share-url": "Share URL",
"label.single-day": "Jedan dan",
"label.steps": "Koraci",
"label.sum": "Suma",
"label.tablet": "Tablet",
"label.team": "Tim",
"label.team-id": "Tim ID",
"label.team-member": "Član tima",
"label.team-name": "Naziv tima",
"label.team-owner": "Vlasnik tima",
"label.team-view-only": "Samo tim može vidjeti",
"label.team-websites": "Timske web stranice",
"label.teams": "Timovi",
"label.theme": "Teme",
"label.this-month": "Ovaj mjesec",
"label.this-week": "Ova sedmica",
"label.this-year": "Ova godina",
"label.timezone": "Vremenska zona",
"label.title": "Naslov",
"label.today": "Danas",
"label.toggle-charts": "Uklj/isklj grafikone",
"label.total": "Ukupno",
"label.total-records": "Ukupno redova",
"label.tracking-code": "Kod za praćenje",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer web stranice",
"label.true": "Da",
"label.type": "Tip",
"label.unique": "Jedinstveno",
"label.unique-visitors": "Jedinstvenih posjetitelja",
"label.unknown": "Nepoznato",
"label.untitled": "Bezimeno",
"label.update": "Update",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "Korisnik",
"label.username": "Korisničko ime",
"label.users": "Korisnici",
"label.utm": "UTM",
"label.utm-description": "Pratite vaše kampanje kroz UTM parametre.",
"label.value": "Vrijednost",
"label.view": "Pregled",
"label.view-details": "Pogledaj detalje",
"label.view-only": "Samo gledanje",
"label.views": "Pregledi",
"label.views-per-visit": "Pregledi po posjeti",
"label.visitors": "Posjetitelji",
"label.visits": "Posjete",
"label.website": "Web stranica",
"label.website-id": "ID web stranice",
"label.websites": "Web stranice",
"label.window": "Prozor",
"label.yesterday": "Jučer",
"message.action-confirmation": "Unesite {confirmation} ispod da potvrdite.",
"message.active-users": "{x} trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
"message.confirm-leave": "Jeste li sigurni da želite napustiti {target}?",
"message.confirm-remove": "Jeste li sigurni da želite ukloniti {target}?",
"message.confirm-reset": "Jeste li sigurni da želite resetovati {target}?",
"message.delete-team-warning": "Brisanje tima će također obrisati sve web stranice tima.",
"message.delete-website-warning": "Svi podaci web stranice biće obrisani.",
"message.error": "Nešto je pošlo po zlu.",
"message.event-log": "{event} na {url}",
"message.go-to-settings": "Idi na postavke",
"message.incorrect-username-password": "Pogrešno korisničko ime i/ili šifra.",
"message.invalid-domain": "Nevalidna domena. Ne uključujte http/https.",
"message.min-password-length": "Minimalna dužina od {n} karaktera",
"message.new-version-available": "Nova verzija Umami {version} je dostupna!",
"message.no-data-available": "Nema dostupnih podataka.",
"message.no-event-data": "Nema dostupnih podataka o događajima.",
"message.no-match-password": "Šifre se ne poklapaju.",
"message.no-results-found": "Nema rezultata.",
"message.no-team-websites": "Ovaj tim nema nikakvih web stranica.",
"message.no-teams": "Niste kreirali nijedan tim.",
"message.no-users": "Nema nikakvih korisnika.",
"message.no-websites-configured": "Nemate iskonfigurisanu nijednu web stranicu.",
"message.page-not-found": "Stranica nije pronađena",
"message.reset-website": "Da resetujete ovu web stranicu, upišite {confirmation} dole da potvrdite.",
"message.reset-website-warning": "Sve statistike o ovoj web stranici će biti obrisane, ali vaše postavke neće biti dirane.",
"message.saved": "Sačuvano.",
"message.share-url": "Statistike vaše web stranice su javno dostupne na sljedećem URLu:",
"message.team-already-member": "Već ste član tima.",
"message.team-not-found": "Tim nije pronađen.",
"message.team-websites-info": "Web stranice može vidjeti bilo ko iz tima.",
"message.tracking-code": "Da pratite statistike ove web stranice, stavite sljedeći kod u <head>...</head> sekciju vašeg HTMLa.",
"message.transfer-team-website-to-user": "Prebacite ovu web stranicu na vaš račun?",
"message.transfer-user-website-to-team": "Odaberite tim u koji želite prebaciti ovu web stranicu.",
"message.transfer-website": "Prebacite vlasništvo web stranice na vaš račun ili drugi tim.",
"message.triggered-event": "Trigerovani događaj",
"message.user-deleted": "Korisnik obrisan.",
"message.viewed-page": "Pogledana stranica",
"message.visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}",
"message.visitors-dropped-off": "Posjetitelji koji su napustili stranicu"
}

View File

@ -1,246 +1,246 @@
{
"label.access-code": "Access code",
"label.access-code": "Codi d'accés",
"label.actions": "Accions",
"label.activity-log": "Activity log",
"label.add": "Add",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Afegeix lloc web",
"label.activity-log": "Registre d'activitat",
"label.add": "Afegir",
"label.add-description": "Afegir descripció",
"label.add-member": "Afegir membre",
"label.add-step": "Afegir pas",
"label.add-website": "Afegir lloc web",
"label.admin": "Administrador",
"label.after": "After",
"label.after": "Després",
"label.all": "Tots",
"label.all-time": "Sempre",
"label.analytics": "Analytics",
"label.average": "Average",
"label.analytics": "Analítiques",
"label.average": "Mitjana",
"label.average-visit-time": "Temps mitjà de visita",
"label.back": "Enrere",
"label.before": "Before",
"label.before": "Abans",
"label.bounce-rate": "Percentatge de rebot",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.breakdown": "Desglossament",
"label.browser": "Navegador",
"label.browsers": "Navegadors",
"label.cancel": "Cancel·la",
"label.change-password": "Canvia la contrasenya",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.cities": "Ciutats",
"label.city": "Ciutat",
"label.clear-all": "Netejar tot",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirma la contrasenya",
"label.contains": "Contains",
"label.continue": "Continue",
"label.contains": "Conté",
"label.continue": "Continuar",
"label.countries": "Països",
"label.country": "Country",
"label.create": "Create",
"label.create-report": "Create report",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
"label.country": "País",
"label.create": "Crear",
"label.create-report": "Crear informe",
"label.create-team": "Crear equip",
"label.create-user": "Crear usuari",
"label.created": "Creat",
"label.created-by": "Creat Per",
"label.current-password": "Contrasenya actual",
"label.custom-range": "Rang personalitzat",
"label.dashboard": "Panell",
"label.data": "Data",
"label.date": "Date",
"label.data": "Dades",
"label.date": "Data",
"label.date-range": "Interval de dates",
"label.day": "Day",
"label.day": "Dia",
"label.default-date-range": "Interval de dates per defecte",
"label.delete": "Esborra",
"label.delete-report": "Delete report",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-report": "Eliminar informe",
"label.delete-team": "Eliminar equip",
"label.delete-user": "Eliminar usuari",
"label.delete-website": "Esborra el lloc web",
"label.description": "Description",
"label.description": "Descripció",
"label.desktop": "Escriptori",
"label.details": "Details",
"label.device": "Device",
"label.details": "Detalls",
"label.device": "Dispositiu",
"label.devices": "Dispositius",
"label.dismiss": "Descarta",
"label.does-not-contain": "Does not contain",
"label.does-not-contain": "No conté",
"label.domain": "Domini",
"label.dropoff": "Dropoff",
"label.dropoff": "Abandonament",
"label.edit": "Edita",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
"label.edit-dashboard": "Edita panell",
"label.edit-member": "Edita membre",
"label.enable-share-url": "Activa l'enllaç per compartir",
"label.event": "Event",
"label.event-data": "Event data",
"label.event": "Esdeveniment",
"label.event-data": "Dades de l'esdeveniment",
"label.events": "Esdeveniments",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
"label.filter": "Filter",
"label.false": "Fals",
"label.field": "Camp",
"label.fields": "Camps",
"label.filter": "Filtre",
"label.filter-combined": "Combinat",
"label.filter-raw": "En cru",
"label.filters": "Filters",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
"label.filters": "Filtres",
"label.funnel": "Embut",
"label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.",
"label.greater-than": "Més gran que",
"label.greater-than-equals": "Més gran que o igual a",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Llengües",
"label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.",
"label.is": "És igual a",
"label.is-not": "No és igual a",
"label.is-not-set": "No està establert",
"label.is-set": "Està establert",
"label.join": "Unir",
"label.join-team": "Unir-se al equip",
"label.language": "Idioma",
"label.languages": "Idiomes",
"label.laptop": "Portàtil",
"label.last-days": "Últims {x} dies",
"label.last-hours": "Últimes {x} hores",
"label.last-months": "Last {x} months",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
"label.last-months": "Últims {x} mesos",
"label.leave": "Abandonar",
"label.leave-team": "Abandonar equip",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor que o igual a",
"label.login": "Connecta't",
"label.logout": "Desconnecta't",
"label.manage": "Manage",
"label.max": "Max",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.manage": "Administrar",
"label.max": "Màx",
"label.member": "Membre",
"label.members": "Membres",
"label.min": "Mín",
"label.mobile": "Mòbil",
"label.more": "Més",
"label.my-account": "My account",
"label.my-websites": "My websites",
"label.my-account": "El meu compte",
"label.my-websites": "Els meus llocs web",
"label.name": "Nom",
"label.new-password": "Contrasenya nova",
"label.none": "None",
"label.none": "Cap",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.os": "OS",
"label.overview": "Overview",
"label.os": "SO",
"label.overview": "Resum",
"label.owner": "Propietari",
"label.page-of": "Page {current} of {total}",
"label.page-of": "Pàgina {current} de {total}",
"label.page-views": "Pàgines vistes",
"label.pageTitle": "Page title",
"label.pageTitle": "Títol de la pàgina",
"label.pages": "Pàgines",
"label.password": "Contrasenya",
"label.powered-by": "Funciona amb {name}",
"label.profile": "Perfil",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.queries": "Consultes",
"label.query": "Consulta",
"label.query-parameters": "Paràmetres de consulta",
"label.realtime": "Temps real",
"label.referrer": "Referrer",
"label.referrer": "Referent",
"label.referrers": "Referents",
"label.refresh": "Refresca",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regenerate": "Regenerar",
"label.region": "Regió",
"label.regions": "Regions",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
"label.remove": "Treure",
"label.remove-member": "Eliminar membre",
"label.reports": "Informes",
"label.required": "Obligatori",
"label.reset": "Restableix",
"label.reset-website": "Restableix estadístiques",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role",
"label.run-query": "Run query",
"label.retention": "Retenció",
"label.retention-description": "Mesuri la retenció del seu lloc web fent un seguiment de la freqüència amb què tornen els usuaris.",
"label.role": "Rol",
"label.run-query": "Executar consulta",
"label.save": "Desa",
"label.screens": "Screens",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.screens": "Pantalles",
"label.search": "Buscar",
"label.select": "Seleccionar",
"label.select-date": "Seleccionar data",
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar lloc web",
"label.sessions": "Sessions",
"label.settings": "Configuració",
"label.share-url": "Enllaç per compartir",
"label.single-day": "Un sol dia",
"label.steps": "Steps",
"label.sum": "Sum",
"label.steps": "Pasos",
"label.sum": "Suma",
"label.tablet": "Tauleta",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
"label.theme": "Theme",
"label.team": "Equip",
"label.team-id": "ID del equip",
"label.team-member": "Membre de l'equip",
"label.team-name": "Nom de l'equip",
"label.team-owner": "Propietari de l'equip",
"label.team-view-only": "Vista només de l'equip",
"label.team-websites": "Llocs web de l'equip",
"label.teams": "Equips",
"label.theme": "Tema",
"label.this-month": "Aquest mes",
"label.this-week": "Aquesta setmana",
"label.this-year": "Aquest any",
"label.timezone": "Zona horària",
"label.title": "Title",
"label.title": "Títol",
"label.today": "Avui",
"label.toggle-charts": "Mostra/amaga gràfics",
"label.total": "Total",
"label.total-records": "Total records",
"label.total-records": "Total de registres",
"label.tracking-code": "Codi de seguiment",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer website",
"label.true": "True",
"label.type": "Type",
"label.unique": "Unique",
"label.transfer": "Transferir",
"label.transfer-website": "Transferir lloc web",
"label.true": "Cert",
"label.type": "Tipus",
"label.unique": "Únic",
"label.unique-visitors": "Visitants únics",
"label.unknown": "Desconegut",
"label.untitled": "Untitled",
"label.update": "Update",
"label.untitled": "Sense títol",
"label.update": "Actualitzar",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "User",
"label.user": "Usuari",
"label.username": "Nom d'usuari",
"label.users": "Users",
"label.users": "Usuaris",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.value": "Value",
"label.view": "View",
"label.utm-description": "Rastreji les seves campanyes a través de paràmetres UTM.",
"label.value": "Valor",
"label.view": "Visualitzar",
"label.view-details": "Veure els detalls",
"label.view-only": "View only",
"label.view-only": "Només veure",
"label.views": "Vistes",
"label.views-per-visit": "Views per visit",
"label.visitors": "Visitants",
"label.visits": "Visits",
"label.website": "Website",
"label.website-id": "Website ID",
"label.visits": "Visites",
"label.website": "Lloc web",
"label.website-id": "ID del lloc web",
"label.websites": "Llocs web",
"label.window": "Window",
"label.window": "Finestra",
"label.yesterday": "Ahir",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.",
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
"message.confirm-delete": "Segur que vols esborrar {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-remove": "Are you sure you want to remove {target}?",
"message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
"message.confirm-delete": "Segur que vol esborrar {target}?",
"message.confirm-leave": "Segur que vol abandonar {target}?",
"message.confirm-remove": "Segur que vol eliminar {target}?",
"message.confirm-reset": "Segur que vol restablir les estadístiques de {target}?",
"message.delete-team-warning": "Al eliminar un equip també s'eliminaran tots els llocs web de l'equip.",
"message.delete-website-warning": "També s'esborraran totes les dades relacionades.",
"message.error": "S'ha produït un error.",
"message.event-log": "{event} on {url}",
"message.event-log": "{event} a {url}",
"message.go-to-settings": "Vés a la configuració",
"message.incorrect-username-password": "Nom d'usuari o contrasenya incorrectes.",
"message.invalid-domain": "Domini invàlid",
"message.min-password-length": "Minimum length of {n} characters",
"message.new-version-available": "A new version of Umami {version} is available!",
"message.min-password-length": "Longitud mínima de {n} caràcters",
"message.new-version-available": "Una nova versió d'Umami {version} està disponible!",
"message.no-data-available": "No hi ha dades disponibles.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "No hi ha dades d'esdeveniments disponibles.",
"message.no-match-password": "Les contrasenyes no coincideixen",
"message.no-results-found": "No results were found.",
"message.no-team-websites": "This team does not have any websites.",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-results-found": "No s'han trobat resultats.",
"message.no-team-websites": "Aquest equip no té cap lloc web.",
"message.no-teams": "No ha creat cap equip.",
"message.no-users": "No hi ha cap usuari.",
"message.no-websites-configured": "No hi ha cap lloc web configurat.",
"message.page-not-found": "No s'ha trobat la pàgina.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website": "Per restablir aquest lloc web, escrigui {confirmation} al cuadre inferior per confirmar.",
"message.reset-website-warning": "S'esborraran totes les estadístiques per aquest lloc web, però el codi de seguiment es mantindrà.",
"message.saved": "S'ha desat amb èxit.",
"message.share-url": "Aquest és l'enllaç públic per compartir de {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
"message.team-already-member": "Ja és membre d'aquest equip.",
"message.team-not-found": "Equip no trobat.",
"message.team-websites-info": "Els llocs web poden ser visualitzats per qualsevol membre de l'equip.",
"message.tracking-code": "Codi de seguiment",
"message.transfer-team-website-to-user": "Transfer this website to your account?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
"message.transfer-team-website-to-user": "Transferir aquest lloc web al seu compte?",
"message.transfer-user-website-to-team": "Seleccioni l'equip al qui transferir aquest lloc web.",
"message.transfer-website": "Transferir la propietat del lloc web al seu compte o a un altre equip.",
"message.triggered-event": "Esdeveniment desencadenat",
"message.user-deleted": "Usuari eliminat.",
"message.viewed-page": "Pàgina vista",
"message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}",
"message.visitors-dropped-off": "Visitors dropped off"
"message.visitors-dropped-off": "Els visitants han sortit"
}

View File

@ -1,246 +1,246 @@
{
"label.access-code": "Access code",
"label.access-code": "Erişim Kodu",
"label.actions": "Hareketler",
"label.activity-log": "Activity log",
"label.add": "Add",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.activity-log": "Aktivite Kaydı",
"label.add": "Ekle",
"label.add-description": "Açıklama ekle",
"label.add-member": "Üye ekle",
"label.add-step": "Adım ekle",
"label.add-website": "Web sitesi ekle",
"label.admin": "Yönetici",
"label.after": "After",
"label.administrator": "Yönetici",
"label.after": "Sonra",
"label.all": "Tümü",
"label.all-time": "All time",
"label.analytics": "Analytics",
"label.average": "Average",
"label.all-time": "Tüm zamanlar",
"label.analytics": "Analitik",
"label.average": "Ortalama",
"label.average-visit-time": "Ortalama ziyaret süresi",
"label.back": "Geri",
"label.before": "Before",
"label.bounce-rate": "Çıkma oranı",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.before": "Önce",
"label.bounce-rate": "Tek sayfa ziyaret oranı",
"label.breakdown": "Dağılım",
"label.browser": "Tarayıcı",
"label.browsers": "Tarayıcılar",
"label.cancel": "İptal",
"label.change-password": "Şifre değiştir",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.cities": "Şehirler",
"label.city": "Şehir",
"label.clear-all": "Hepsini temizle",
"label.confirm": "Onayla",
"label.confirm-password": "Parolayı onayla",
"label.contains": "Contains",
"label.continue": "Continue",
"label.contains": "İçeriği",
"label.continue": "Devam et",
"label.countries": "Ülkeler",
"label.country": "Country",
"label.create": "Create",
"label.create-report": "Create report",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
"label.country": "Ülke",
"label.create": "Oluştur",
"label.create-report": "Rapor oluştur",
"label.create-team": "Takım oluştur",
"label.create-user": "Kullanıcı oluştur",
"label.created": "Oluşturuldu",
"label.created-by": "Tarafından oluşturldu",
"label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık",
"label.dashboard": "Kontrol Paneli",
"label.data": "Data",
"label.date": "Date",
"label.data": "Veri",
"label.date": "Tarih",
"label.date-range": "Tarih aralığı",
"label.day": "Day",
"label.day": "Gün",
"label.default-date-range": "Varsayılan tarih aralığı",
"label.delete": "Sil",
"label.delete-report": "Delete report",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-report": "Rapor sil",
"label.delete-team": "Takım sil",
"label.delete-user": "Kullanıcı sil",
"label.delete-website": "Web sitesini sil",
"label.description": "Description",
"label.description": "ıklama",
"label.desktop": "Masaüstü",
"label.details": "Details",
"label.device": "Device",
"label.details": "Detaylar",
"label.device": "Cihaz",
"label.devices": "Cihazlar",
"label.dismiss": "Reddet",
"label.does-not-contain": "Does not contain",
"label.does-not-contain": "İçermez",
"label.domain": "Alan adı",
"label.dropoff": "Dropoff",
"label.dropoff": "Bırakma",
"label.edit": "Düzenle",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
"label.edit-dashboard": "Kontrol panelini düzenle",
"label.edit-member": "Üyeyi düzenle",
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
"label.event": "Event",
"label.event-data": "Event data",
"label.event": "Olay",
"label.event-data": "Olay verisi",
"label.events": "Olaylar",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
"label.filter": "Filter",
"label.filter-combined": "Birleşik",
"label.filter-raw": "Ham",
"label.filters": "Filters",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
"label.false": "Yanlış",
"label.field": "Alan",
"label.fields": "Alanlar",
"label.filter": "Filtre",
"label.filter-combined": "Birleşik filtre",
"label.filter-raw": "Ham filtre",
"label.filters": "Filtreler",
"label.funnel": "Huni",
"label.funnel-description": "Kullanıcıların dönüşüm ve ayrılma oranlarını anlayın.",
"label.greater-than": "Büyüktür",
"label.greater-than-equals": "Büyük veya eşittir",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.insights-description": "Segmentleri ve filtreleri kullanarak verilerinizi derinlemesine inceleyin.",
"label.is": "Is",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Languages",
"label.is-not": "Değil",
"label.is-not-set": "Ayarlanmamış",
"label.is-set": "Ayarlandı",
"label.join": "Katıl",
"label.join-team": "Takıma katıl",
"label.language": "Dil",
"label.languages": "Diller",
"label.laptop": "Dizüstü",
"label.last-days": "Son {x} gün",
"label.last-hours": "Son {x} saat",
"label.last-months": "Last {x} months",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
"label.last-months": "Son {x} ay",
"label.leave": "Ayrıl",
"label.leave-team": "Takımdan Ayrıl",
"label.less-than": "Küçüktür",
"label.less-than-equals": "Küçük veya eşittir",
"label.login": "Giriş Yap",
"label.logout": ıkış Yap",
"label.manage": "Manage",
"label.manage": "Yönet",
"label.max": "Max",
"label.member": "Member",
"label.members": "Members",
"label.member": "Üye",
"label.members": "Üyeler",
"label.min": "Min",
"label.mobile": "Mobil Cihaz",
"label.more": "Detaylı göster",
"label.my-account": "My account",
"label.my-websites": "My websites",
"label.my-account": "Hesabım",
"label.my-websites": "Web sitelerim",
"label.name": "İsim",
"label.new-password": "Yeni parola",
"label.none": "None",
"label.none": "Yok",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.ok": "TAMAM",
"label.os": "OS",
"label.overview": "Overview",
"label.owner": "Owner",
"label.page-of": "Page {current} of {total}",
"label.overview": "Genel bakış",
"label.owner": "Sahibi",
"label.page-of": "{total} sayfada {current} ",
"label.page-views": "Sayfa görünümü",
"label.pageTitle": "Page title",
"label.pageTitle": "Sayfa başlığı",
"label.pages": "Sayfalar",
"label.password": "Parola",
"label.powered-by": "Sağlayıcı: {name}",
"label.profile": "Profil",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.queries": "Sorgular",
"label.query": "Sorgu",
"label.query-parameters": "Sorgu parametreleri",
"label.realtime": "Gerçek Zamanlı",
"label.referrer": "Referrer",
"label.referrers": "Yönlendirenler",
"label.refresh": "Yenile",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
"label.regenerate": "Yeniden Oluştur",
"label.region": "Bölge",
"label.regions": "Bölgeler",
"label.remove": "Kaldır",
"label.remove-member": "Üyeyi kaldır",
"label.reports": "Raporlar",
"label.required": "Zorunlu alan",
"label.reset": "Sıfırla",
"label.reset-website": "Reset statistics",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role",
"label.run-query": "Run query",
"label.reset-website": "İstatistikleri sıfırla",
"label.retention": "Geri dönüş",
"label.retention-description": "Kullanıcıların ne sıklıkla geri döndüğünü takip ederek web sitenizin kalıcılığını ölçün.",
"label.role": "Rol",
"label.run-query": "Sorgu çalıştır",
"label.save": "Kaydet",
"label.screens": "Ekranlar",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.search": "Ara",
"label.select": "Seç",
"label.select-date": "Tarih seç",
"label.select-role": "Rol seç",
"label.select-website": "Web sitesi seç",
"label.sessions": "Sessions",
"label.settings": "Ayarlar",
"label.share-url": "Paylaşım adresi",
"label.single-day": "Tekil gün",
"label.steps": "Steps",
"label.sum": "Sum",
"label.steps": "Adımlar",
"label.sum": "Toplam",
"label.tablet": "Tablet",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
"label.theme": "Theme",
"label.team": "Takım",
"label.team-id": "Takım ID",
"label.team-member": "Takım üyesi",
"label.team-name": "Takım ismi",
"label.team-owner": "Takım sahibi",
"label.team-view-only": "Yalnızca ekip görünümü",
"label.team-websites": "Takım web siteleri",
"label.teams": "Takımlar",
"label.theme": "Tema",
"label.this-month": "Bu ay",
"label.this-week": "Bu hafta",
"label.this-year": "Bu yıl",
"label.timezone": "Zaman dilimi",
"label.title": "Title",
"label.title": "Başlık",
"label.today": "Bugün",
"label.toggle-charts": "Toggle charts",
"label.total": "Total",
"label.total-records": "Total records",
"label.toggle-charts": "Grafikleri değiştir",
"label.total": "Toplam",
"label.total-records": "Toplam kayıt",
"label.tracking-code": "İzleme kodu",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer website",
"label.true": "True",
"label.type": "Type",
"label.unique": "Unique",
"label.transfer-website": "Transfer web sitesi",
"label.true": "Doğru",
"label.type": "Tip",
"label.unique": "Benzersiz",
"label.unique-visitors": "Tekil kullanıcı",
"label.unknown": "Bilinmeyen",
"label.untitled": "Untitled",
"label.update": "Update",
"label.untitled": "İsimsiz",
"label.update": "Güncelle",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "User",
"label.user": "Kullanıcı",
"label.username": "Kullanıcı adı",
"label.users": "Users",
"label.users": "Kullanıcılar",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.value": "Value",
"label.view": "View",
"label.utm-description": "Kampanyalarınızı UTM parametreleri aracılığıyla takip edin.",
"label.value": "Değer",
"label.view": "Görünüm",
"label.view-details": "Detayı incele",
"label.view-only": "View only",
"label.view-only": "Sadece görünüm",
"label.views": "Görüntüleme",
"label.views-per-visit": "Views per visit",
"label.views-per-visit": "Ziyaret başına görüntüleme",
"label.visitors": "Ziyaretçi",
"label.visits": "Visits",
"label.website": "Website",
"label.visits": "Ziyaretler",
"label.website": "Web sitesi",
"label.website-id": "Website ID",
"label.websites": "Web siteleri",
"label.window": "Window",
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"label.window": "Pencere",
"label.yesterday": "Dün",
"message.action-confirmation": "Onaylamak için aşağıdaki kutuya {confirmation} yazın.",
"message.active-users": "{x} aktif ziyaretçi",
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-remove": "Are you sure you want to remove {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.confirm-leave": "{target} kaydından ayrılmak istediğinizden emin misiniz?",
"message.confirm-remove": "{target} kaydını kaldırmak istediğinizden emin misiniz?",
"message.confirm-reset": "{target} istatistiklerini sıfırlamak istediğinizden emin misiniz?",
"message.delete-team-warning": "Bir takımı silmek tüm takım web sitelerini de silecektir.",
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.error": "Bir şeyler ters gitti!",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Ayarlara git",
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
"message.invalid-domain": "Geçersiz alan adı",
"message.min-password-length": "Minimum length of {n} characters",
"message.new-version-available": "A new version of Umami {version} is available!",
"message.min-password-length": "Minimum {n} karakter uzunluğu",
"message.new-version-available": "Yeni versiyon Umami {version} mevcut!",
"message.no-data-available": "Henüz hiç veri yok.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "Hiçbir olay verisi mevcut değil.",
"message.no-match-password": "Parolalar uyuşmuyor",
"message.no-results-found": "No results were found.",
"message.no-team-websites": "This team does not have any websites.",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-results-found": "Hiçbir sonuç bulunamadı.",
"message.no-team-websites": "Bu takımın herhangi bir web sitesi yok.",
"message.no-teams": "Herhangi bir takım oluşturmadınız.",
"message.no-users": "Kullanıcı yok.",
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
"message.page-not-found": "Sayfa bulunamadı.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-website": "Bu websitesini sıfılamak için aşağıdaki kutuya {confirmation} yazın.",
"message.reset-website-warning": "Bu web sitesi için tüm istatistikler silinecek, ancak izleme kodunuz bozulmadan kalacaktır.",
"message.saved": "Başarıyla kaydedildi.",
"message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
"message.team-already-member": "Zaten bu takımın üyesisiniz",
"message.team-not-found": "Takım bulunamadı",
"message.team-websites-info": "Web siteleri takımdaki herkes tarafından görüntülenebilir.",
"message.tracking-code": "İzleme kodu",
"message.transfer-team-website-to-user": "Transfer this website to your account?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
"message.transfer-team-website-to-user": "Bu web sitesi hesbınıza aktarılsın mı?",
"message.transfer-user-website-to-team": "Bu web sitesinin aktarılacağı takımı seçin.",
"message.transfer-website": "Web sitesi sahipliğini hesabınıza veya başka bir takıma aktarın",
"message.triggered-event": "Tetiklenen olay",
"message.user-deleted": "Kullanıcı silindi.",
"message.viewed-page": "Görüntülenen sayfa",
"message.visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}",
"message.visitors-dropped-off": "Visitors dropped off"
"message.visitors-dropped-off": "Bırakan ziyaretçiler"
}

View File

@ -5,7 +5,7 @@
"label.add": "添加",
"label.add-description": "添加描述",
"label.add-member": "添加成员",
"label.add-step": "Add step",
"label.add-step": "添加步骤",
"label.add-website": "添加网站",
"label.admin": "管理员",
"label.after": "之后",
@ -90,7 +90,7 @@
"label.laptop": "笔记本",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小时",
"label.last-months": "Last {x} months",
"label.last-months": "最近 {x} 个月",
"label.leave": "离开",
"label.leave-team": "离开团队",
"label.less-than": "少于",
@ -152,7 +152,7 @@
"label.settings": "设置",
"label.share-url": "共享链接",
"label.single-day": "单日",
"label.steps": "Steps",
"label.steps": "步骤",
"label.sum": "总和",
"label.tablet": "平板",
"label.team": "团队",
@ -182,22 +182,22 @@
"label.unique-visitors": "独立访客",
"label.unknown": "未知",
"label.untitled": "未命名",
"label.update": "Update",
"label.update": "更新",
"label.url": "网址",
"label.urls": "网址",
"label.user": "用户",
"label.username": "用户名",
"label.users": "用户",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.utm-description": "通过UTM参数追踪您的广告活动。",
"label.value": "值",
"label.view": "查看",
"label.view-details": "查看更多",
"label.view-only": "仅浏览量",
"label.views": "浏览量",
"label.views-per-visit": "Views per visit",
"label.views-per-visit": "每次访问的浏览量",
"label.visitors": "访客",
"label.visits": "Visits",
"label.visits": "访问次数",
"label.website": "网站",
"label.website-id": "网站 ID",
"label.websites": "网站",

View File

@ -1,82 +0,0 @@
import { User, Website } from '@prisma/client';
import redis from '@umami/redis-client';
import { getSession, getUser, getWebsite } from '../queries';
async function fetchWebsite(websiteId: string): Promise<Website> {
return redis.client.getCache(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
}
async function storeWebsite(data: { id: any }) {
const { id } = data;
const key = `website:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteWebsite(id) {
return redis.client.deleteCache(`website:${id}`);
}
async function fetchUser(id): Promise<User> {
return redis.client.getCache(`user:${id}`, () => getUser(id, { includePassword: true }), 86400);
}
async function storeUser(data) {
const { id } = data;
const key = `user:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteUser(id) {
return redis.client.deleteCache(`user:${id}`);
}
async function fetchSession(id) {
return redis.client.getCache(`session:${id}`, () => getSession(id), 86400);
}
async function storeSession(data) {
const { id } = data;
const key = `session:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteSession(id) {
return redis.client.deleteCache(`session:${id}`);
}
async function fetchUserBlock(userId: string) {
const key = `user:block:${userId}`;
return redis.client.get(key);
}
async function incrementUserBlock(userId: string) {
const key = `user:block:${userId}`;
return redis.client.incr(key);
}
export default {
fetchWebsite,
storeWebsite,
deleteWebsite,
fetchUser,
storeUser,
deleteUser,
fetchSession,
storeSession,
deleteSession,
fetchUserBlock,
incrementUserBlock,
enabled: !!redis.enabled,
};

View File

@ -4,7 +4,7 @@ import debug from 'debug';
import { CLICKHOUSE } from 'lib/db';
import { QueryFilters, QueryOptions } from './types';
import { OPERATORS } from './constants';
import { loadWebsite } from './load';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { filtersToArray } from './params';
@ -106,7 +106,7 @@ function getFilterParams(filters: QueryFilters = {}) {
}
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
return {
filterQuery: getFilterQuery(filters, options),

View File

@ -30,7 +30,7 @@ export const FILTER_DAY = 'filter-day';
export const FILTER_RANGE = 'filter-range';
export const FILTER_REFERRERS = 'filter-referrers';
export const FILTER_PAGES = 'filter-pages';
export const UNIT_TYPES = ['year', 'month', 'hour', 'day'];
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event'];
export const SESSION_COLUMNS = [
@ -132,6 +132,7 @@ export const ROLES = {
user: 'user',
viewOnly: 'view-only',
teamOwner: 'team-owner',
teamManager: 'team-manager',
teamMember: 'team-member',
teamViewOnly: 'team-view-only',
} as const;
@ -162,6 +163,12 @@ export const ROLE_PERMISSIONS = {
PERMISSIONS.websiteUpdate,
PERMISSIONS.websiteDelete,
],
[ROLES.teamManager]: [
PERMISSIONS.teamUpdate,
PERMISSIONS.websiteCreate,
PERMISSIONS.websiteUpdate,
PERMISSIONS.websiteDelete,
],
[ROLES.teamMember]: [
PERMISSIONS.websiteCreate,
PERMISSIONS.websiteUpdate,

View File

@ -1,6 +1,6 @@
import { startOfHour, startOfMonth } from 'date-fns';
import { hash } from 'next-basics';
import { v4, v5, validate } from 'uuid';
import { v4, v5 } from 'uuid';
export function secret() {
return hash(process.env.APP_SECRET || process.env.DATABASE_URL);
@ -23,7 +23,3 @@ export function uuid(...args: any) {
return v5(hash(...args, salt()), v5.DNS);
}
export function isUuid(value: string) {
return validate(value);
}

View File

@ -132,7 +132,7 @@ export async function getClientInfo(req: NextApiRequestCollect) {
const subdivision2 = location?.subdivision2;
const city = location?.city;
const browser = browserName(userAgent);
const os = detectOS(userAgent);
const os = detectOS(userAgent) as string;
const device = getDevice(req.body?.payload?.screen, os);
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };

View File

@ -2,6 +2,7 @@ import {
arSA,
be,
bn,
bs,
cs,
sk,
da,
@ -48,6 +49,7 @@ export const languages = {
'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' },
'be-BY': { label: 'Беларуская', dateLocale: be },
'bn-BD': { label: 'বাংলা', dateLocale: bn },
'bs-BA': { label: 'Bosanski', dateLocale: bs },
'ca-ES': { label: 'Català', dateLocale: ca },
'cs-CZ': { label: 'Čeština', dateLocale: cs },
'da-DK': { label: 'Dansk', dateLocale: da },

View File

@ -1,12 +1,12 @@
import cache from 'lib/cache';
import { getSession, getUser, getWebsite } from 'queries';
import { User, Website, Session } from '@prisma/client';
import { getSession, getWebsite } from 'queries';
import { Website, Session } from '@prisma/client';
import redis from '@umami/redis-client';
export async function loadWebsite(websiteId: string): Promise<Website> {
export async function fetchWebsite(websiteId: string): Promise<Website> {
let website;
if (cache.enabled) {
website = await cache.fetchWebsite(websiteId);
if (redis.enabled) {
website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
} else {
website = await getWebsite(websiteId);
}
@ -18,11 +18,11 @@ export async function loadWebsite(websiteId: string): Promise<Website> {
return website;
}
export async function loadSession(sessionId: string): Promise<Session> {
export async function fetchSession(sessionId: string): Promise<Session> {
let session;
if (cache.enabled) {
session = await cache.fetchSession(sessionId);
if (redis.enabled) {
session = await redis.client.fetch(`session:${sessionId}`, () => getSession(sessionId), 86400);
} else {
session = await getSession(sessionId);
}
@ -33,19 +33,3 @@ export async function loadSession(sessionId: string): Promise<Session> {
return session;
}
export async function loadUser(userId: string): Promise<User> {
let user;
if (cache.enabled) {
user = await cache.fetchUser(userId);
} else {
user = await getUser(userId);
}
if (!user || user.deletedAt) {
return null;
}
return user;
}

View File

@ -4,13 +4,12 @@ import redis from '@umami/redis-client';
import { getAuthToken, parseShareToken } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { secret } from 'lib/crypto';
import { findSession } from 'lib/session';
import { getSession } from 'lib/session';
import {
badRequest,
createMiddleware,
forbidden,
notFound,
parseSecureToken,
tooManyRequest,
unauthorized,
} from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
@ -27,7 +26,7 @@ export const useCors = createMiddleware(
export const useSession = createMiddleware(async (req, res, next) => {
try {
const session = await findSession(req as NextApiRequestCollect);
const session = await getSession(req as NextApiRequestCollect);
if (!session) {
log('useSession: Session not found');
@ -36,11 +35,8 @@ export const useSession = createMiddleware(async (req, res, next) => {
(req as any).session = session;
} catch (e: any) {
if (e.message === 'Usage Limit.') {
return tooManyRequest(res, e.message);
}
if (e.message.startsWith('Website not found:')) {
return forbidden(res, e.message);
if (e.message.startsWith('Website not found')) {
return notFound(res, e.message);
}
return badRequest(res, e.message);
}

View File

@ -3,9 +3,9 @@ import prisma from '@umami/prisma-client';
import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
import { loadWebsite } from './load';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { QueryFilters, QueryOptions, SearchFilter } from './types';
import { QueryFilters, QueryOptions, PageParams } from './types';
import { filtersToArray } from './params';
const MYSQL_DATE_FORMATS = {
@ -152,7 +152,7 @@ async function parseFilters(
filters: QueryFilters = {},
options: QueryOptions = {},
) {
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key));
return {
@ -191,7 +191,7 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return prisma.rawQuery(query, params);
}
async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;

View File

@ -2,7 +2,7 @@ import * as yup from 'yup';
export const dateRange = {
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
};
export const pageInfo = {

View File

@ -1,28 +1,13 @@
import { isUuid, secret, uuid, visitSalt } from 'lib/crypto';
import { secret, uuid, visitSalt } from 'lib/crypto';
import { getClientInfo } from 'lib/detect';
import { parseToken } from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
import { createSession } from 'queries';
import cache from './cache';
import clickhouse from './clickhouse';
import { loadSession, loadWebsite } from './load';
import { fetchSession, fetchWebsite } from './load';
import { SessionData } from 'lib/types';
export async function findSession(req: NextApiRequestCollect): Promise<{
id: any;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: any;
device: string;
screen: string;
language: string;
country: any;
subdivision1: any;
subdivision2: any;
city: any;
ownerId: string;
}> {
export async function getSession(req: NextApiRequestCollect): Promise<SessionData> {
const { payload } = req.body;
if (!payload) {
@ -35,9 +20,8 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
if (cacheToken) {
const result = await parseToken(cacheToken, secret());
// Token is valid
if (result) {
await checkUserBlock(result?.ownerId);
return result;
}
}
@ -45,25 +29,13 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
// Verify payload
const { website: websiteId, hostname, screen, language } = payload;
// Check the hostname value for legality to eliminate dirty data
const validHostnameRegex = /^[\w-.]+$/;
if (!validHostnameRegex.test(hostname)) {
throw new Error('Invalid hostname.');
}
if (!isUuid(websiteId)) {
throw new Error('Invalid website ID.');
}
// Find website
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
if (!website) {
throw new Error(`Website not found: ${websiteId}.`);
}
await checkUserBlock(website.userId);
const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } =
await getClientInfo(req);
@ -78,7 +50,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
visitId,
hostname,
browser,
os: os as any,
os,
device,
screen,
language,
@ -86,12 +58,11 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
subdivision1,
subdivision2,
city,
ownerId: website.userId,
};
}
// Find session
let session = await loadSession(sessionId);
let session = await fetchSession(sessionId);
// Create a session if not found
if (!session) {
@ -117,13 +88,5 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
}
}
return { ...session, ownerId: website.userId, visitId: visitId };
}
async function checkUserBlock(userId: string) {
if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) {
await cache.incrementUserBlock(userId);
throw new Error('Usage Limit.');
}
return { ...session, visitId: visitId };
}

View File

@ -24,31 +24,7 @@ export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
export interface WebsiteSearchFilter extends SearchFilter {
userId?: string;
teamId?: string;
includeTeams?: boolean;
onlyTeams?: boolean;
}
export interface UserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface TeamSearchFilter extends SearchFilter {
userId?: string;
}
export interface TeamUserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface ReportSearchFilter extends SearchFilter {
userId?: string;
websiteId?: string;
}
export interface SearchFilter {
export interface PageParams {
query?: string;
page?: number;
pageSize?: number;
@ -56,7 +32,7 @@ export interface SearchFilter {
sortDescending?: boolean;
}
export interface FilterResult<T> {
export interface PageResult<T> {
data: T;
count: number;
page: number;
@ -66,10 +42,10 @@ export interface FilterResult<T> {
}
export interface FilterQueryResult<T> {
result: FilterResult<T>;
result: PageResult<T>;
query: any;
params: SearchFilter;
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
params: PageParams;
setParams: Dispatch<SetStateAction<T | PageParams>>;
}
export interface DynamicData {
@ -230,3 +206,19 @@ export interface RealtimeData {
countries?: any[];
visitors?: any[];
}
export interface SessionData {
id: string;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: string;
device: string;
screen: string;
language: string;
country: string;
subdivision1: string;
subdivision2: string;
city: string;
}

View File

@ -1,13 +1,13 @@
import { canViewUsers } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUsers } from 'queries';
import * as yup from 'yup';
export interface UsersRequestQuery extends SearchFilter {}
export interface UsersRequestQuery extends PageParams {}
export interface UsersRequestBody {
userId: string;
username: string;

View File

@ -1,13 +1,17 @@
import { canViewAllWebsites } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsites } from 'queries';
import * as yup from 'yup';
import { pageInfo } from 'lib/schema';
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestQuery extends PageParams {
userId?: string;
includeTeams?: boolean;
}
export interface WebsitesRequestBody {
name: string;
@ -39,8 +43,29 @@ export default async (
return unauthorized(res);
}
const { userId, includeOwnedTeams } = req.query;
const websites = await getWebsites(
{
where: {
OR: [
...(userId && [{ userId }]),
...(userId &&
includeOwnedTeams && [
{
team: {
deletedAt: null,
teamUser: {
some: {
role: ROLES.teamOwner,
userId,
},
},
},
},
]),
],
},
include: {
user: {
select: {
@ -48,6 +73,18 @@ export default async (
id: true,
},
},
team: {
where: {
deletedAt: null,
},
include: {
teamUser: {
where: {
role: ROLES.teamOwner,
},
},
},
},
},
},
req.query,

View File

@ -17,7 +17,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
event: yup.string(),
}),
};

View File

@ -17,7 +17,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
field: yup.string(),
}),
};

View File

@ -16,7 +16,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
}),
};

View File

@ -1,10 +1,5 @@
import ipaddr from 'ipaddr.js';
import { isbot } from 'isbot';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types';
import { NextApiRequest, NextApiResponse } from 'next';
import {
badRequest,
@ -15,6 +10,11 @@ import {
safeDecodeURI,
send,
} from 'next-basics';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types';
import { saveEvent, saveSessionData } from 'queries';
import * as yup from 'yup';
@ -41,7 +41,6 @@ export interface NextApiRequestCollect extends NextApiRequest {
id: string;
websiteId: string;
visitId: string;
ownerId: string;
hostname: string;
browser: string;
os: string;
@ -98,7 +97,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
}
const { type, payload } = req.body;
const { url, referrer, name: eventName, data, title } = payload;
const { url, referrer, name: eventName, data, title, website} = payload;
const pageTitle = safeDecodeURI(title);
await useSession(req, res);
@ -146,6 +145,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
...session,
sessionId: session.id,
visitId: session.visitId,
websiteId: session.websiteId || website,
});
}

View File

@ -23,7 +23,7 @@ const schema = {
POST: yup.object().shape({
role: yup
.string()
.matches(/team-member|team-view-only/i)
.matches(/team-member|team-view-only|team-manager/i)
.required(),
}),
};

View File

@ -1,13 +1,13 @@
import { canAddUserToTeam, canViewTeam } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamUser, getTeamUser, getTeamUsers } from 'queries';
import * as yup from 'yup';
export interface TeamUserRequestQuery extends SearchFilter {
export interface TeamUserRequestQuery extends PageParams {
teamId: string;
}
@ -25,7 +25,7 @@ const schema = {
userId: yup.string().uuid().required(),
role: yup
.string()
.matches(/team-member|team-view-only/i)
.matches(/team-member|team-view-only|team-manager/i)
.required(),
}),
};

View File

@ -1,13 +1,13 @@
import * as yup from 'yup';
import { canViewTeam } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { ok, unauthorized } from 'next-basics';
import { getTeamWebsites } from 'queries';
export interface TeamWebsiteRequestQuery extends SearchFilter {
export interface TeamWebsiteRequestQuery extends PageParams {
teamId: string;
}

View File

@ -3,13 +3,13 @@ import { Team } from '@prisma/client';
import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam } from 'queries';
export interface TeamsRequestQuery extends SearchFilter {}
export interface TeamsRequestQuery extends PageParams {}
export interface TeamsRequestBody {
name: string;
}

View File

@ -1,12 +1,12 @@
import * as yup from 'yup';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUserTeams } from 'queries';
export interface UserTeamsRequestQuery extends SearchFilter {
export interface UserTeamsRequestQuery extends PageParams {
userId: string;
}

View File

@ -26,7 +26,7 @@ const schema = {
GET: yup.object().shape({
id: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref<number>('startAt')).required(),
endAt: yup.number().integer().min(yup.ref<number>('startAt')).required(),
}),
};

View File

@ -2,14 +2,14 @@ import { canCreateUser } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createUser, getUserByUsername } from 'queries';
import * as yup from 'yup';
export interface UsersRequestQuery extends SearchFilter {}
export interface UsersRequestQuery extends PageParams {}
export interface UsersRequestBody {
username: string;
password: string;

View File

@ -1,6 +1,6 @@
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { getRequestDateRange } from 'lib/request';
import { getRequestFilters, getRequestDateRange } from 'lib/request';
import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types';
import { TimezoneTest, UnitTypeTest } from 'lib/yup';
import { NextApiResponse } from 'next';
@ -15,16 +15,32 @@ export interface WebsiteEventsRequestQuery {
unit?: string;
timezone?: string;
url: string;
referrer?: string;
title?: string;
os?: string;
browser?: string;
device?: string;
country?: string;
region: string;
city?: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
unit: UnitTypeTest,
timezone: TimezoneTest,
url: yup.string(),
referrer: yup.string(),
title: yup.string(),
os: yup.string(),
browser: yup.string(),
device: yup.string(),
country: yup.string(),
region: yup.string(),
city: yup.string(),
}),
};
@ -36,7 +52,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId, timezone, url } = req.query;
const { websiteId, timezone } = req.query;
const { startDate, endDate, unit } = await getRequestDateRange(req);
if (req.method === 'GET') {
@ -44,13 +60,15 @@ export default async (
return unauthorized(res);
}
const events = await getEventMetrics(websiteId, {
const filters = {
...getRequestFilters(req),
startDate,
endDate,
timezone,
unit,
url,
});
};
const events = await getEventMetrics(websiteId, filters);
return ok(res, events);
}

View File

@ -1,13 +1,13 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsiteReports } from 'queries';
import { pageInfo } from 'lib/schema';
export interface ReportsRequestQuery extends SearchFilter {
export interface ReportsRequestQuery extends PageParams {
websiteId: string;
}

View File

@ -1,7 +1,7 @@
import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createWebsite } from 'queries';
@ -9,7 +9,7 @@ import userWebsitesRoute from 'pages/api/users/[userId]/websites';
import * as yup from 'yup';
import { pageInfo } from 'lib/schema';
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestQuery extends PageParams {}
export interface WebsitesRequestBody {
name: string;

View File

@ -1,6 +1,6 @@
import { Prisma, Report } from '@prisma/client';
import prisma from 'lib/prisma';
import { FilterResult, ReportSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> {
@ -17,8 +17,8 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports(
criteria: ReportFindManyArgs,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
filters: PageParams = {},
): Promise<PageResult<Report[]>> {
const { query } = filters;
const where: Prisma.ReportWhereInput = {
@ -50,8 +50,8 @@ export async function getReports(
export async function getUserReports(
userId: string,
filters?: ReportSearchFilter,
): Promise<FilterResult<Report[]>> {
filters?: PageParams,
): Promise<PageResult<Report[]>> {
return getReports(
{
where: {
@ -72,8 +72,8 @@ export async function getUserReports(
export async function getWebsiteReports(
websiteId: string,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
filters: PageParams = {},
): Promise<PageResult<Report[]>> {
return getReports(
{
where: {

View File

@ -2,7 +2,7 @@ import { Prisma, Team } from '@prisma/client';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> {
@ -22,8 +22,8 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
export async function getTeams(
criteria: TeamFindManyArgs,
filters: TeamSearchFilter = {},
): Promise<FilterResult<Team[]>> {
filters: PageParams = {},
): Promise<PageResult<Team[]>> {
const { getSearchParameters } = prisma;
const { query } = filters;
@ -42,7 +42,7 @@ export async function getTeams(
);
}
export async function getUserTeams(userId: string, filters: TeamSearchFilter = {}) {
export async function getUserTeams(userId: string, filters: PageParams = {}) {
return getTeams(
{
where: {

View File

@ -1,7 +1,7 @@
import { Prisma, TeamUser } from '@prisma/client';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamUserSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs;
export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> {
@ -19,8 +19,8 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers(
criteria: TeamUserFindManyArgs,
filters?: TeamUserSearchFilter,
): Promise<FilterResult<TeamUser[]>> {
filters?: PageParams,
): Promise<PageResult<TeamUser[]>> {
const { query } = filters;
const where: Prisma.TeamUserWhereInput = {

View File

@ -1,8 +1,7 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import { ROLES } from 'lib/constants';
import prisma from 'lib/prisma';
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
import { PageResult, Role, User, PageParams } from 'lib/types';
import { getRandomChars } from 'next-basics';
import UserFindManyArgs = Prisma.UserFindManyArgs;
@ -50,8 +49,8 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers(
criteria: UserFindManyArgs,
filters?: UserSearchFilter,
): Promise<FilterResult<User[]>> {
filters?: PageParams,
): Promise<PageResult<User[]>> {
const { query } = filters;
const where: Prisma.UserWhereInput = {
@ -221,15 +220,5 @@ export async function deleteUser(
id: userId,
},
}),
]).then(async data => {
if (cache.enabled) {
const ids = websites.map(a => a.id);
for (let i = 0; i < ids.length; i++) {
await cache.deleteWebsite(`website:${ids[i]}`);
}
}
return data;
});
]);
}

View File

@ -1,7 +1,6 @@
import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise<Website> {
@ -26,8 +25,8 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites(
criteria: WebsiteFindManyArgs,
filters: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters: PageParams,
): Promise<PageResult<Website[]>> {
const { query } = filters;
const where: Prisma.WebsiteWhereInput = {
@ -54,8 +53,8 @@ export async function getAllWebsites(userId: string) {
export async function getUserWebsites(
userId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters?: PageParams,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
where: {
@ -79,8 +78,8 @@ export async function getUserWebsites(
export async function getTeamWebsites(
teamId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters?: PageParams,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
where: {
@ -102,17 +101,9 @@ export async function getTeamWebsites(
export async function createWebsite(
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
): Promise<Website> {
return prisma.client.website
.create({
data,
})
.then(async data => {
if (cache.enabled) {
await cache.storeWebsite(data);
}
return data;
});
return prisma.client.website.create({
data,
});
}
export async function updateWebsite(
@ -148,13 +139,7 @@ export async function resetWebsite(
resetAt: new Date(),
},
}),
]).then(async data => {
if (cache.enabled) {
await cache.storeWebsite(data[3]);
}
return data;
});
]);
}
export async function deleteWebsite(
@ -188,11 +173,5 @@ export async function deleteWebsite(
: client.website.delete({
where: { id: websiteId },
}),
]).then(async data => {
if (cache.enabled) {
await cache.deleteWebsite(websiteId);
}
return data;
});
]);
}

View File

@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
and event_data.created_at between {{startDate}} and {{endDate}}
and website_event.event_name = {{event}}
group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 4 desc
order by 1 asc, 2 asc, 3 asc, 5 desc
`,
params,
);
@ -81,7 +81,7 @@ async function clickhouseQuery(
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_name = {event:String}
group by data_key, data_type, string_value, event_name
order by 1 asc, 2 asc, 3 asc, 4 desc
order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500
`,
params,

View File

@ -25,12 +25,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
`
select
event_name x,
${getDateQuery('created_at', unit, timezone)} t,
${getDateQuery('website_event.created_at', unit, timezone)} t,
count(*) y
from website_event
${joinSession}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2

View File

@ -1,5 +1,4 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
export async function createSession(data: Prisma.SessionCreateInput) {
@ -18,28 +17,20 @@ export async function createSession(data: Prisma.SessionCreateInput) {
city,
} = data;
return prisma.client.session
.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
})
.then(async data => {
if (cache.enabled) {
await cache.storeSession(data);
}
return data;
});
return prisma.client.session.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
});
}

1122
yarn.lock

File diff suppressed because it is too large Load Diff