Compare commits
47 Commits
d63e6ad879
...
f2f53eeb87
Author | SHA1 | Date |
---|---|---|
榆木 | f2f53eeb87 | |
Mike Cao | 8be95e32ba | |
Mike Cao | ad72828000 | |
Mike Cao | 84d7089c15 | |
Mike Cao | 222e3d0705 | |
Francis Cao | 9c06bc9893 | |
Mike Cao | b8cab762ca | |
Mike Cao | ca2a7f3354 | |
Mike Cao | 8b5e690c3b | |
Mike Cao | 63fe57321c | |
Mike Cao | ceac4eb43f | |
Mike Cao | c4e3327dd8 | |
Mike Cao | cf6a37f0f2 | |
mobeicanyue | abb7da8c95 | |
Mike Cao | 5f3998789d | |
Mike Cao | 85968d5c7c | |
Mike Cao | 1b85c56e65 | |
Mike Cao | 467b8a1b6b | |
Mike Cao | e1315c0c10 | |
Mike Cao | 89f8b5e8ce | |
Mike Cao | e0510a815e | |
Mike Cao | de9b6c9173 | |
Mike Cao | dd1795f08c | |
Mike Cao | ab94f5d1cd | |
Mike Cao | 2ba87cd483 | |
Brian Cao | 4eb01c5563 | |
Brian Cao | 4e2bfd7485 | |
Mike Cao | 0fc8828f8b | |
Mike Cao | 216304a191 | |
Mike Cao | 439f8a8aa3 | |
Mike Cao | 2620e8fe26 | |
Mike Cao | 592c181902 | |
Francis Cao | cfbc4ebd72 | |
Francis Cao | fde2be4900 | |
Francis Cao | 64d9a196cc | |
Francis Cao | 4b40b4e9cf | |
Francis Cao | 758b289a51 | |
mobeicanyue | 436f55c1a5 | |
Sakib Hadziavdic | 5aa9b20ff7 | |
Rui Bin Choo | 22ab8b8222 | |
Celil6p | 81283ee9e4 | |
榆木 | b379834e5c | |
Mike Cao | 4ca4be4445 | |
Mike Cao | 32cfb74c49 | |
Gerard Nesta | f4310be501 | |
面条 | c28de0fb1b | |
Gerard Nesta | f3283d693d |
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
@ -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": [
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/reports/utm/page';
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/websites/[websiteId]/event-data/page';
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/websites/[websiteId]/realtime/page';
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/websites/[websiteId]/reports/page';
|
||||
|
||||
export default Page;
|
|
@ -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({
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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' },
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": "Açı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"
|
||||
}
|
||||
|
|
|
@ -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": "网站",
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue